コード例 #1
0
class MainWindow(QMainWindow):

    # Create a QSettings object rather than storing values. Pass in a company 
    # name and an application name
    settings = QSettings("Custom GUIs", "Image Manager GUI")
    #print(settings.fileName()) # NOTE: Uncomment to print the path to settings

    images_path = "Images" # File path to the Images directory
    image_dir = QDir(images_path)
    info_dialog = None # Create variable for modeless dialog

    def __init__(self):
        """MainWindow Constructor for Image Manager"""
        super().__init__() # Constructor for QMainWindow
        self.initializeUI()

    def initializeUI(self):
        """Set up the GUI's main window and load initial settings and data."""
        self.setWindowTitle("Image Manager")
        self.setObjectName("ImageManager")

        # Set up the main window, menu, dock widgets, and initialize the GUI's settings
        self.setUpMainWindow()
        self.displayImagePreviewDock()
        self.createActions()
        self.createMenus()
        self.loadStoredImageData()
        self.getInitialSettings()
        self.show() # Display the main window

    def setUpMainWindow(self):
        """Set up the application's main window containing the QListWidget."""
        self.image_view_lw = ImageViewerListWidget(self)
        # Use signals/slots to interact with the list widget 
        self.image_view_lw.itemSelectionChanged.connect(self.updateDockInfo)
        self.image_view_lw.itemDoubleClicked.connect(self.displayImageInfoDialog)
        # Use the list widget's internal model to enable/disable menu items
        self.image_view_lw.model().rowsInserted.connect(self.manageMenuItems)
        self.image_view_lw.model().rowsRemoved.connect(self.manageMenuItems)

        self.setCentralWidget(self.image_view_lw)

    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.import_act = QAction("Import Images...", self, triggered=self.importImages)
        self.import_act.setShortcut("Ctrl+I") 

        self.preferences_act = QAction("Preferences...", self, triggered=self.showPreferencesDialog)

        self.quit_act = QAction("Quit Task Manager", self, triggered=self.close)
        self.quit_act.setShortcut(QKeySequence.StandardKey.Quit) # Ctrl+Q

        # Create actions for Edit menu
        self.select_all_act = QAction("Select All", self, triggered=self.image_view_lw.selectAll)
        self.select_all_act.setShortcut(QKeySequence.StandardKey.SelectAll) # Ctrl+A

        self.delete_act = QAction("Delete Images", self, triggered=self.deleteImages)
        self.delete_act.setShortcut(QKeySequence.StandardKey.Delete) # Del
        self.delete_act.setEnabled(False)

        # Create actions for View menu
        # Handle the visibility of the dock widget that displays images
        self.show_dock_act = self.image_preview_dock.toggleViewAction()
        self.show_dock_act.setText("Show Image View")  

        self.sort_ascend_act = QAction("Sort Ascending", self,
            triggered=lambda: self.sortListItems(Qt.SortOrder.AscendingOrder))
        self.sort_ascend_act.setEnabled(False)

        self.sort_descend_act = QAction("Sort Descending", self,
            triggered=lambda: self.sortListItems(Qt.SortOrder.DescendingOrder))
        self.sort_descend_act.setEnabled(False)

        self.fullscreen_act = QAction("Show Fullscreen", self, 
            triggered=self.displayFullScreen, checkable=True)

        # Create actions for Help menu
        self.about_act = QAction("About Image Manager", 
            self, triggered=self.showAboutDialog)  

    def createMenus(self):
        """Create the application's menu."""
        if QSysInfo.productType() == "macos" or "osx":
            self.menuBar().setNativeMenuBar(False)

        self.file_menu = self.menuBar().addMenu("&File")
        self.file_menu.addAction(self.import_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.preferences_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.quit_act)

        self.edit_menu = self.menuBar().addMenu("&Edit")
        self.edit_menu.addAction(self.select_all_act)
        self.edit_menu.addSeparator()
        self.edit_menu.addAction(self.delete_act)  

        self.view_menu = self.menuBar().addMenu("&View")
        self.view_menu.addAction(self.show_dock_act)  
        self.view_menu.addSeparator()
        self.view_menu.addAction(self.sort_ascend_act)
        self.view_menu.addAction(self.sort_descend_act)
        self.view_menu.addSeparator()
        self.view_menu.addAction(self.fullscreen_act)       

        self.help_menu = self.menuBar().addMenu("&Help")
        self.help_menu.addAction(self.about_act)        

    def manageMenuItems(self, parent, first, last):
        """Slot to enable/disable menu items if rows have been 
        added/deleted to QListWidget. The rowsInserted() and 
        rowsRemoved() that trigger this slot return the 'parent',
        'first', and 'last' values, but they are not used in 
        this method."""
        if self.image_view_lw.count() == 0:
            self.delete_act.setEnabled(False)
            self.sort_ascend_act.setEnabled(False)
            self.sort_descend_act.setEnabled(False)
        elif self.image_view_lw.count() > 0:
            self.delete_act.setEnabled(True)
            self.sort_ascend_act.setEnabled(True)
            self.sort_descend_act.setEnabled(True)   

    def displayImagePreviewDock(self):
        """Dock widget that displays a selected image in a scrollable 
        area and uses its file name as the dock's title."""
        self.image_preview_dock = QDockWidget()
        self.image_preview_dock.setObjectName("PreviewDock")
        self.image_preview_dock.setWindowTitle("Show Image View")
        self.image_preview_dock.setAllowedAreas(Qt.DockWidgetAreas.RightDockWidgetArea)

        self.display_image_label = QLabel()
        self.display_image_label.setAlignment(Qt.Alignment.AlignCenter)

        self.view_scroll_area = QScrollArea()
        self.view_scroll_area.setMinimumWidth(300)
        self.view_scroll_area.setWidgetResizable(True)

        self.image_preview_dock.setWidget(self.view_scroll_area)
        # Set initial location of dock widget in the main window
        self.addDockWidget(Qt.DockWidgetAreas.RightDockWidgetArea, self.image_preview_dock)   

    def updateDockInfo(self):
        """Slot to update the image that the dock widget displays."""
        # Only display an image if one item is selected
        if (len(self.image_view_lw.selectedItems()) == 0 or 
            len(self.image_view_lw.selectedItems()) > 1):
            self.image_preview_dock.setWindowTitle("Show Image View")
            self.display_image_label.clear()
        else:
            curr_item = self.image_view_lw.currentItem()
            self.image_preview_dock.setWindowTitle(curr_item.text())
            self.show_dock_act.setText("Show Image View") 

            # Get the current height of the dock widget
            dock_height = self.image_preview_dock.height()
            # Get the size of the original image/item
            icon_size = curr_item.icon().availableSizes()[0]
            icon_width = icon_size.width()

            # Return a pixmap from the item's icon and display in the scroll area
            pixmap = curr_item.icon().pixmap(QSize(icon_width, dock_height)) 
            self.display_image_label.setPixmap(pixmap)
            self.view_scroll_area.setWidget(self.display_image_label) 
        
    def importImages(self):
        """Import the images a user selects, remove duplicates, and add
        items to the QListWidget."""
        duplicate_images = [] # Store the names of duplicate images
        image_paths, _ = QFileDialog.getOpenFileNames(self, 
            "Select Image Files", "", "Images (*.png *.xpm *.jpg *.jpeg)")

        if image_paths:
            if self.image_dir.exists():
                for image_path in image_paths:
                    # Pass image path to QFileInfo object
                    image_info = QFileInfo(image_path) 
                    file_name = image_info.fileName()
                    item_name = image_info.baseName()

                    # Copy the files into the Images directory, check for files 
                    # with the same name
                    new_name = self.image_dir.absolutePath() + f"/{file_name}"
                    file_exists = QFile.copy(image_path, new_name)
                    if file_exists == False:
                        duplicate_images.append(image_path)
                    else:
                        self.createListItems(image_path, item_name, image_info, new_name)
                        if self.is_delete_checked == True: # Handle deleting images
                            QFile.moveToTrash(image_path) 
            else:
                QMessageBox.warning(self, "Images Location Not Found",
                    """<p>The Images Location cannot be found. Restart the application to
                    recreate the directory.</p>""")

        # Display a custom dialog to inform the user of duplicate images
        if len(duplicate_images) != 0:
            duplicates_dialog = QMessageBox(self)
            duplicates_dialog.setIcon(QMessageBox.Icon.Information)
            duplicates_dialog.setWindowTitle("Duplicate Images")
            duplicates_dialog.setText("""<p>Some images were not imported because 
                they already exist.</p>""")

            details = '\n'.join([item for item in duplicate_images])
            duplicates_dialog.setDetailedText(details)
            duplicates_dialog.exec()

            duplicate_images.clear() # Clear the list 
        # Check if window is still in focus. If not, give it focus
        if self.isActiveWindow() == False:
            self.activateWindow()

    def createListItems(self, image_path, item_name, image_info, new_name=None):
        """Simple method for creating QListWidgetItem objects. 
        'image_path': the path to the file.
        'item_name': the base name used for QListWidgetItem objects.
        'image_info': the QFileInfo object.
        'new_name': used when importing new photos, making sure the program
        points to the new image location."""
        list_item = QListWidgetItem(QIcon(image_path), item_name)
        self.image_view_lw.setIconSize(QSize(80, 80))
        self.image_view_lw.addItem(list_item)
        if new_name != None:
            image_info.setFile(new_name)
        self.image_view_lw.images_info_list.append(image_info) 

    def sortListItems(self, order): 
        """First, sort the items in the QListWidget using sortItems(). Then handle 
        sorting the QFileInfo objects in the images_info_list using Python's sort() to 
        match how the QListWidget sorts items."""
        self.image_view_lw.sortItems(order)
        if order == Qt.SortOrder.AscendingOrder:
            self.image_view_lw.images_info_list.sort(key=lambda item: (item.baseName().upper(), item.baseName()[0].islower()))
        elif order == Qt.SortOrder.DescendingOrder:
            self.image_view_lw.images_info_list.sort(reverse=True, key=lambda item: (item.baseName().upper(), item.baseName()[0].islower()))

    def deleteImages(self):
        """Delete images from the QListWidget and from where images
        are stored on disk."""
        number_of_photos = len(self.image_view_lw.selectedItems())
        answer = QMessageBox.warning(self, "Delete Image(s)", 
            f"Are you sure you want to delete {number_of_photos} image(s)?", 
            QMessageBox.StandardButtons.No | QMessageBox.StandardButtons.Yes, 
            QMessageBox.StandardButtons.No)

        if answer == QMessageBox.StandardButtons.Yes:
            for item in self.image_view_lw.selectedItems():
                index = self.image_view_lw.indexFromItem(item).row()
                # Get the image's information before deletion
                image_info = self.image_view_lw.images_info_list[index] 

                # Remove items from the Images directory, from the list widget, 
                # and the images_info_list that stores QFileInfo objects
                QFile.moveToTrash(image_info.absoluteFilePath()) 
                self.image_view_lw.takeItem(index)
                del self.image_view_lw.images_info_list[index] 
                del item        

    def loadStoredImageData(self):
        """Load images from the Images directory. The Images directory is 
        created the first time running the application."""
        if not(self.image_dir.exists()):
            QDir().mkdir(self.images_path)
        elif self.image_dir.exists():
            # Create a list of the files in the Images directory
            images = self.image_dir.entryInfoList(QDir.Filters.AllEntries | QDir.Filters.NoDotAndDotDot)
            for image in images: 
                # Imported files are QFileInfo objects
                item_name = image.baseName()
                path = image.absoluteFilePath()
                self.createListItems(path, item_name, image) 

    def displayImageInfoDialog(self, item): 
        """Display image metadata in a modeless dialog box. 'index' is the index of 
        the item that is clicked on."""
        index = self.image_view_lw.indexFromItem(item).row()
        if self.info_dialog == None: 
            self.info_dialog = ImageInfoDialog(self, self.image_view_lw.images_info_list[index])
        elif self.info_dialog != None:
            self.info_dialog.close()
            self.info_dialog = ImageInfoDialog(self, self.image_view_lw.images_info_list[index])
        self.info_dialog.show()         

    def showPreferencesDialog(self):
        """Display the application's preferences dialog. Save the value of the 
        delete_images_checkbox in the settings."""
        prefs_dialog = PreferencesDialog(self, self.image_dir, self.is_delete_checked)
        response = prefs_dialog.exec()

        if response == 1: # QDialog.DialogCode.Accepted == 1
            self.settings.setValue("delete_images", prefs_dialog.delete_images_checkbox.isChecked())
            self.is_delete_checked = self.settings.value("delete_images", type=bool)

    def displayFullScreen(self, state):
        """Check the state of checkable fullscreen_act. If True, show the 
        main window as fullscreen."""
        if state: self.showFullScreen()
        else: self.showNormal()

    def showAboutDialog(self):
        """Display the application's about dialog."""
        QMessageBox.about(self, "Image Manager",
            """<h3 style='text-align:center'>Image Manager</h3>
            <p style='font-weight: normal'>The <b><i>Image Manager GUI</i></b> 
            demonstrates how to build an application for managing photos. This 
            program also examines some of the common features found in many GUIs.</p>
            <p style='font-weight: normal'>This application is part of
            <b><i>Building Custom UIs with PyQt</i></b>.</p>
            <p style='font-weight: normal'>Designed by: <b>Joshua Willman</b></p>
            <p style='font-weight: normal'>Icons created by: <b>Joshua Willman</b></p>""")

    def getInitialSettings(self):
        """Get initial settings of the application using QSettings upon startup."""
        position = self.settings.value("position", QPoint(200, 0))
        size = self.settings.value("size", QSize(800, 500))
        self.is_delete_checked = self.settings.value("delete_images", type=bool) 
        # restoreState() is used here to restore the image_preview_dock widget
        self.restoreState(self.settings.value("window_state", bytes(QByteArray())))

        self.resize(size)
        self.move(position)
        return self.is_delete_checked
    
    def saveSettings(self):
        """Save the settings of the application."""
        self.settings.setValue("position", self.pos())
        self.settings.setValue("size", self.size())
        self.settings.setValue("window_state", self.saveState())

    def closeEvent(self, event):
        """Save the application's settings in the closeEvent()."""
        self.saveSettings()
        event.setAccepted(True)
コード例 #2
0
class MainWindow(QMainWindow):

    def __init__(self):
        """ MainWindow Constructor """
        super().__init__()
        self.initializeUI()
        
    def initializeUI(self):
        """Set up the GUI's main window."""        
        self.setMinimumSize(700, 400)
        self.setWindowTitle("GIF and Image Viewer")

        # Set up the main window, menu, and dock widget
        self.setUpMainWindow()
        self.displayFilesDock()
        self.createActions()
        self.createMenus()
        self.createToolbar()
        self.show() # Display the main window

    def setUpMainWindow(self):
        """Set up the application's main window and widgets."""
        self.movie = QMovie() # Create movie object
        self.movie.stateChanged.connect(self.changeButtonStates)
        
        self.media_label = QLabel() # Create label to place images/GIFs on
        self.media_label.setPixmap(QPixmap("icons/image_label.png"))
        self.media_label.setFrameShape(QFrame.Shape.StyledPanel)
        self.media_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # Prevent the label from resizing and affecting the 
        # dock widget when viewing images
        self.media_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)

        self.setCentralWidget(self.media_label)

    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.open_act = QAction("Open...", self, triggered=self.openDirectory)
        self.open_act.setShortcut(QKeySequence.StandardKey.Open) 

        self.quit_act = QAction("Quit Viewer", self, triggered=self.close)
        self.quit_act.setShortcut(QKeySequence.StandardKey.Quit) # Ctrl+Q

        # Create actions for View menu
        # Handle the visibility of the dock widget
        self.show_dock_act = self.files_dock.toggleViewAction()
        self.show_dock_act.setText("Show Media Folder") 

        # Create actions for the toolbar (These actions could also be 
        # added to the GUI's menu bar or to a context menu)
        self.play_act = QAction(QIcon("icons/play.png"), "Play", self, triggered=self.startMovie)
        self.pause_act = QAction(QIcon("icons/pause.png"), "Pause", self, triggered=self.pauseMovie)
        self.stop_act = QAction(QIcon("icons/stop.png"), "Stop/Reset", self, triggered=self.stopMovie)
        self.disableMovieButtons()

    def createMenus(self):
        """Create the application's menu."""
        # Make the toolbar appear in the main window for macOS users.
        # More information about this in Chapter 2 - Building the Foundation for GUIs
        if QSysInfo.productType() == "macos" or "osx":
            self.menuBar().setNativeMenuBar(False)

        self.file_menu = self.menuBar().addMenu("&File")
        self.file_menu.addAction(self.open_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.quit_act)

        self.view_menu = self.menuBar().addMenu("&View")
        self.view_menu.addAction(self.show_dock_act)      

    def createToolbar(self):
        """Create the application's toolbar for playing GIFs."""
        toolbar = self.addToolBar("GIF Controls Toolbar")
        toolbar.setIconSize(QSize(24, 24))

        # Add actions to the toolbar
        toolbar.addAction(self.play_act)
        toolbar.addAction(self.pause_act)
        toolbar.addAction(self.stop_act)

    def displayFilesDock(self):
        """Dock widget that displays the movie file location in a QLineEdit 
        widget, provides a button for opening directories with images and GIFs, 
        and shows the media from the selected folder in a QTreeWidget."""
        self.files_dock = QDockWidget()
        self.files_dock.setWindowTitle("Media Folder")
        self.files_dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea)

        folder_label = QLabel("Media Location:")
        # The QLineEdit widget is set to read-only as a quick way to display 
        # the folder path
        self.folder_line = QLineEdit()
        self.folder_line.setMinimumWidth(100)
        self.folder_line.setReadOnly(True)

        open_button = QPushButton("Open...")
        open_button.clicked.connect(self.openDirectory)

        folder_h_box = QHBoxLayout()
        folder_h_box.addWidget(folder_label)
        folder_h_box.addWidget(self.folder_line)
        folder_h_box.addWidget(open_button)

        self.files_tree = QTreeWidget()
        self.files_tree.setHeaderLabel("Media Files")
        self.files_tree.setColumnCount(1)
        self.files_tree.itemSelectionChanged.connect(self.displayMediaFile)

        # Set up the dock's layout
        dock_v_box = QVBoxLayout()
        dock_v_box.addLayout(folder_h_box)
        dock_v_box.addWidget(self.files_tree)

        dock_container = QWidget()
        dock_container.setLayout(dock_v_box)

        self.files_dock.setWidget(dock_container)
        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.files_dock)

    def openDirectory(self):
        """Open a QFileDialog for selecting a local directory. Only display 
        image and GIF files.""" 
        directory = QFileDialog.getExistingDirectory(self, "Choose Directory", "", 
            QFileDialog.Option.ShowDirsOnly) # Specify the file mode to only select directories

        if directory:
            # Display the selected folder text in the QLineEdit. There are other, and 
            # possibly better, ways to handle displaying the text, but for simplicity, 
            # this is the method used in this GUI
            self.folder_line.setText(directory)

            # Get the contents of the directory and only display files with
            # specified extensions
            file_dir = QDir(directory)
            filters = ["*.gif", "*.png", "*.jpg", "*.jpeg"]
            files_info = file_dir.entryInfoList(filters, QDir.Filter.Files)

            # Clear the contents of the QTreeWidget
            if self.files_tree.model().rowCount() > 0:         
                # NOTE: Since files_tree is connected to the itemSelectionChanged signal,
                # using the clear() method below will also cause the signal to be emitted. 
                # This causes an undesired issue because of how the items are deleted. To avoid
                # the signal being called, QObject.blockSignals() will halt files_tree from 
                # emitting signals while removing items
                self.files_tree.blockSignals(True)
                # Use the convenience method clear() provided by QTreeWidget to remove 
                # all items and selections
                self.files_tree.clear() 
                self.files_tree.blockSignals(False)

                # Reset the QLabel and its image, and disable the movie buttons (in case the 
                # last item selected was a GIF)
                self.media_label.clear()
                self.media_label.setPixmap(QPixmap("icons/image_label.png"))
                self.disableMovieButtons()

            # Create items for each file and add them to the tree
            for file in files_info:
                item = QTreeWidgetItem()
                item.setText(0, file.fileName())
                self.files_tree.addTopLevelItem(item)

    def displayMediaFile(self): 
        """Display the selected media file on the QLabel. Used instead of 
        the itemClicked signal to handle whether the user clicks on an item
        or if arrow keys are used to navigate the items in the tree."""  
        # Check the state of the movie, i.e. check if a movie is playing. If it is, 
        # stop the movie
        if self.movie.state() == QMovie.MovieState.Running:
            self.stopMovie()

        # Get the text from the QLineEdit, folder_line, and concatenate it with 
        # the selected item's text
        column = self.files_tree.currentColumn()
        media_location = self.folder_line.text() + "/" + self.files_tree.selectedItems()[0].text(column)
        
        if media_location.split(".")[1] == "gif":
            self.movie.setFileName(media_location)
            # Check if image data is valid before playing
            if self.movie.isValid(): 
                # Use setMovie() to set the label's contents as the selected GIF
                self.media_label.setMovie(self.movie)
                self.startMovie() # Call method to begin playing
        else:
            # Disable all buttons when an image is selected
            self.disableMovieButtons()

            # Set the label's pixmap (have the image fit the current size of the image label)
            self.media_label.setPixmap(QPixmap(media_location).scaled(self.media_label.size(), 
                Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))  

    def startMovie(self):
        """Start playing the movie."""
        self.movie.start() 

    def pauseMovie(self):
        """Pause the movie."""
        self.movie.setPaused(True)

    def stopMovie(self):
        """Stop playing the movie and reset the movie back to 
        the first frame."""
        self.movie.stop()
        self.movie.jumpToFrame(0)

    def changeButtonStates(self, state):
        """Slot that handles enabling/disabling buttons in the toolbar
        based on the state of QMovie."""
        if state == QMovie.MovieState.Running:
            # The animation begins playing once control returns to the event loop
            self.play_act.setEnabled(False)
            self.pause_act.setEnabled(True)
            self.stop_act.setEnabled(True)
        if state == QMovie.MovieState.Paused:
            self.play_act.setEnabled(True)
            self.pause_act.setEnabled(False)
            self.stop_act.setEnabled(False)
        if state == QMovie.MovieState.NotRunning:
            self.play_act.setEnabled(True)
            self.pause_act.setEnabled(False)
            self.stop_act.setEnabled(False)

    def disableMovieButtons(self):
        """Simple method to disable the movie buttons in the toolbar."""
        self.play_act.setEnabled(False)
        self.pause_act.setEnabled(False)
        self.stop_act.setEnabled(False)