def init_ui(self): glay_lup = QHBoxLayout() glay_lup.addWidget(self.btn_fo) glay_lup.addWidget(self.btn_pid) glay_lup.addWidget(self.btn_uid) glay_lup.addWidget(self.btn_snao) glay_ldown = QGridLayout() glay_ldown.addWidget(QLabel('画廊ID'), 0, 0, 1, 1) glay_ldown.addWidget(self.ledit_pid, 0, 1, 1, 5) glay_ldown.addWidget(QLabel('用户ID'), 1, 0, 1, 1) glay_ldown.addWidget(self.ledit_uid, 1, 1, 1, 5) glay_ldown.addWidget(QLabel('数量'), 2, 0, 1, 1) glay_ldown.addWidget(self.ledit_num, 2, 1, 1, 1) glay_ldown.setColumnStretch(1, 1) glay_ldown.setColumnStretch(2, 5) vlay_left = QVBoxLayout() vlay_left.addLayout(glay_lup) vlay_left.addLayout(glay_ldown) hlay_thumb = QHBoxLayout() hlay_thumb.addLayout(vlay_left) hlay_thumb.addWidget(self.thumbnail) left_wid = QWidget() left_wid.setLayout(hlay_thumb) self.thumbnail.setFixedHeight(left_wid.sizeHint().height()) self.thumbnail.setFixedWidth(150) self.thumbnail.setPixmap(self.thumb_default) if self.show_thumb_flag: self.thumbnail.show() else: self.thumbnail.hide() vlay_right = QVBoxLayout() vlay_right.addWidget(self.user_info, alignment=Qt.AlignHCenter) vlay_right.addWidget(self.btn_logout) vlay_right.addWidget(self.btn_get) vlay_right.addWidget(self.btn_dl) right_wid = QWidget() right_wid.setLayout(vlay_right) splitter = QSplitter(Qt.Horizontal) splitter.setHandleWidth(3) splitter.addWidget(left_wid) splitter.addWidget(right_wid) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 0) splitter.handle(1).setDisabled(True) vlay_main = QVBoxLayout() vlay_main.addWidget(splitter) vlay_main.addWidget(self.table_viewer) self.setLayout(vlay_main)
class MyMainWindow(QWidget): def __init__(self): super().__init__() self.title = "PyQt5 Splitter" self.top = 200 self.left = 500 self.width = 400 self.height = 300 hbox = QHBoxLayout() self.widg_1 = QFrame() self.widg_1.setFrameShape(QFrame.StyledPanel) self.my_splitter = QSplitter(Qt.Horizontal) self.widg_2 = QLineEdit() self.my_splitter.addWidget(self.widg_1) self.my_splitter.addWidget(self.widg_2) self.my_splitter.setSizes([200, 200]) hbox.addWidget(self.my_splitter) # Note: splitter handle 0 is always hidden and handle 1 is between the widgets 1 & 2 self.my_splitter_handle = self.my_splitter.handle(1) self.my_splitter.setStyleSheet( "QSplitter::handle {background: 3px blue};") self.setLayout(hbox) self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.width, self.height) self.show()
class originalWindow(QWidget): def __init__(self): QWidget.__init__(self) self.splitter = QSplitter(self) self.splitter.addWidget(QTextEdit(self)) self.splitter.addWidget(QTextEdit(self)) layout = QVBoxLayout(self) layout.addWidget(self.splitter) handle = self.splitter.handle(1) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) button = QToolButton(handle) button.setArrowType(QtCore.Qt.LeftArrow) button.clicked.connect( lambda: self.handleSplitterButton(True)) layout.addWidget(button) button = QToolButton(handle) button.setArrowType(QtCore.Qt.RightArrow) button.clicked.connect( lambda: self.handleSplitterButton(False)) layout.addWidget(button) handle.setLayout(layout) def handleSplitterButton(self, left=True): if not all(self.splitter.sizes()): self.splitter.setSizes([1, 1]) elif left: self.splitter.setSizes([0, 1]) else: self.splitter.setSizes([1, 0])
class DockWidget(QDockWidget): def __init__(self, parent=None, window_name=None, widget1=None, widget2=None, button=None, hide_button_name=None): super(DockWidget, self).__init__(window_name, parent) self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable) dock_widgets = HorizontalWidget(widget1=widget1, widget2=widget2) v_layout = QVBoxLayout() v_layout.addWidget(dock_widgets, alignment=Qt.AlignCenter) if button: self.button = button v_layout.addWidget(self.button, alignment=Qt.AlignBottom) if hide_button_name: self.hide_button = QPushButton(str(hide_button_name)) self.splitter = QSplitter(Qt.Horizontal) button_layout = QVBoxLayout() button_layout.addWidget(self.hide_button, alignment=Qt.AlignCenter) set_widget = QWidget() set_widget.setLayout(button_layout) self.splitter.addWidget(set_widget) set_widget = QWidget() set_widget.setLayout(v_layout) self.splitter.addWidget(set_widget) self.splitter.setSizes([1, 0]) self.splitter.show() handle = self.splitter.handle(1) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.switch_frame = QAction(handle) self.switch_frame.triggered.connect(self.handle_splitter) self.hide_button.clicked.connect(self.switch_frame.trigger) self.setWidget(self.splitter) else: set_widget = QWidget() set_widget.setLayout(v_layout) self.setWidget(set_widget) self.show() def handle_splitter(self): if self.splitter.sizes()[0] > 0: self.splitter.setSizes([0, 1]) else: self.splitter.setSizes([1, 0]) def closeEvent(self, event): event.ignore() if self.isFloating(): self.setFloating(False) if self.splitter.sizes()[1] > 0: self.splitter.setSizes([1, 0])
def __init__(self): super().__init__() self.language = settings.LANG self.csv_separator = settings.CSV_SEPARATOR self.fmt_float = settings.FMT_FLOAT self.logging_level = settings.LOGGING_LEVEL self.panel = MainPanel(self) config_button = QPushButton('Global\nConfiguration') config_button.setMinimumHeight(40) config_button.clicked.connect(self.global_config) pageList = QListWidget() for name in ['Start', 'Extract variables', 'Max/Min/Mean/Arrival/Duration', 'Interpolate on points', 'Interpolate along lines', 'Project along lines', 'Project mesh', 'Compute volume', 'Compute flux', 'Compare two results', 'Transform coordinate systems', 'Convert geom file formats', 'Variable Calculator']: pageList.addItem('\n' + name + '\n') pageList.setFlow(QListView.TopToBottom) pageList.currentRowChanged.connect(self.panel.layout().setCurrentIndex) pageList.setCurrentRow(0) splitter = QSplitter() left_widget = QWidget() vlayout = QVBoxLayout() vlayout.addWidget(config_button) vlayout.addWidget(pageList) left_widget.setLayout(vlayout) splitter.addWidget(left_widget) splitter.addWidget(self.panel) splitter.setHandleWidth(5) splitter.setCollapsible(0, False) splitter.setCollapsible(1, False) handle = splitter.handle(1) layout = QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) line = QFrame() line.setFrameShape(QFrame.VLine) line.setFrameShadow(QFrame.Sunken) layout.addWidget(line) handle.setLayout(layout) mainLayout = QHBoxLayout() mainLayout.addWidget(splitter) mainLayout.setContentsMargins(0, 0, 0, 0) self.setLayout(mainLayout) self.setWindowTitle('PyTelTools :: Classic interface') self.setWindowFlags(self.windowFlags() | Qt.CustomizeWindowHint) self.frameGeom = self.frameGeometry() self.move(self.frameGeom.center())
class SplitterFrameWidget(QWidget): status_message = pyqtSignal(str, name='status_message') def __init__(self, parent, widget1, widget2, orientation='horizontal', arrow_direction='right'): super(SplitterFrameWidget, self).__init__(parent) if orientation == 'horizontal': orientation = Qt.Horizontal else: orientation = Qt.Vertical if arrow_direction == 'right': arrow = Qt.RightArrow else: arrow = Qt.LeftArrow self.splitter = QSplitter(orientation) self.splitter.addWidget(self.center_widget(widget1)) self.splitter.addWidget(widget2) self.splitter.setSizes([1, 0]) layout = QVBoxLayout(self) layout.addWidget(self.splitter, alignment=Qt.AlignBottom) handle = self.splitter.handle(1) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) button = QToolButton(handle) button.setText("Save") button.setCheckable(True) button.setArrowType(arrow) button.clicked.connect(self.handle_splitter_button) layout.addWidget(button, alignment=Qt.AlignCenter) handle.setLayout(layout) self.center_frame() self.show() def handle_splitter_button(self): if not all(self.splitter.sizes()): self.splitter.setSizes([1, 1]) else: self.splitter.setSizes([1, 0]) def center_widget(self, widget): centered_widget = QWidget() temp_layout = QHBoxLayout() temp_layout.addWidget(widget, alignment=Qt.AlignCenter) centered_widget.setLayout(temp_layout) return centered_widget def center_frame(self): screen_dimensions = self.geometry() center_dimensions = QDesktopWidget().availableGeometry().topLeft() screen_dimensions.moveCenter(center_dimensions) self.move(screen_dimensions.center())
def decorate(splitter: QSplitter, index: int = 1): gripLength = 35 gripWidth = 1 # may need to be 1 or 2 depending on theme gripSpacing = 0 grips = 3 splitter.setOpaqueResize(False) splitter.setChildrenCollapsible(False) splitter.setHandleWidth(7) handle = splitter.handle(index) orientation = splitter.orientation() layout = QHBoxLayout(handle) layout.setSpacing(gripSpacing) layout.setContentsMargins(0, 0, 0, 0) if orientation == Qt.Horizontal: for i in range(grips): line = QFrame(handle) line.setMinimumSize(gripWidth, gripLength) line.setMaximumSize(gripWidth, gripLength) line.setLineWidth(gripWidth) line.setFrameShape(line.StyledPanel) line.setStyleSheet("border: 1px solid lightgray;") layout.addWidget(line) else: # center the vertical grip by adding spacers before and after layout.addStretch() vBox = QVBoxLayout() for i in range(grips): line = QFrame(handle) line.setMinimumSize(gripLength, gripWidth) line.setMaximumSize(gripLength, gripWidth) line.setFrameShape(line.StyledPanel) line.setStyleSheet("border: 1px solid lightgray;") vBox.addWidget(line) layout.addLayout(vBox) layout.addStretch()
class Window(QWidget): def __init__(self): QWidget.__init__(self) self.splitter = QSplitter( QtCore.Qt.Vertical, self) self.splitter.addWidget(QTextEdit(self)) self.splitter.addWidget(QTextEdit(self)) layout = QHBoxLayout(self) layout.addWidget(self.splitter) handle = self.splitter.handle(1) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) button = QToolButton(handle) button.setArrowType(QtCore.Qt.UpArrow) button.clicked.connect( lambda: self.handleSplitterButton(True,[0,1])) layout.addWidget(button) button = QToolButton(handle) button.setArrowType(QtCore.Qt.DownArrow) button.clicked.connect( lambda: self.handleSplitterButton(False,[0,1])) layout.addWidget(button) handle.setLayout(layout) def handleSplitterButton(self, up=True, lines = [0,1]): if not all(self.splitter.sizes()): self.splitter.setSizes([1, 1, 1]) elif up: self.splitter.setSizes([0, 1, 1]) else: self.splitter.setSizes([1, 0, 1])
class Model(QWidget): def __init__(self, list_of_widgets): QWidget.__init__(self) self.splitter = QSplitter(QtCore.Qt.Vertical, self) for widget in list_of_widgets: scroll = QScrollArea() scroll.setWidget(widget) scroll.setWidgetResizable(True) self.splitter.addWidget(scroll) # scroll.setFixedHeight(400) # layout = QtGui.QVBoxLayout(self) # layout.addWidget(scroll) # self.splitter.addWidget(QTextEdit(self)) # self.splitter.addWidget(QTextEdit(self)) layout = QHBoxLayout(self) layout.addWidget(self.splitter) handle = self.splitter.handle(1) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) handle.setLayout(layout)
class SubTabWidget(QWidget): _tabChanged = pyqtSignal(int, name = "tabChanged") def __init__(self, subtitleData, videoWidget, parent = None): super(SubTabWidget, self).__init__(parent) self._subtitleData = subtitleData self.__initTabWidget(videoWidget) def __initTabWidget(self, videoWidget): settings = SubSettings() mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setSpacing(0) #TabBar self.tabBar = QTabBar(self) # Splitter (bookmarks + pages) self.splitter = QSplitter(self) self.splitter.setObjectName("sidebar_splitter") self._toolbox = ToolBox(self._subtitleData, self) self._toolbox.setObjectName("sidebar") self._toolbox.setMinimumWidth(100) self._toolbox.addTool(Details(self._subtitleData, self)) self._toolbox.addTool(Synchronizer(videoWidget, self._subtitleData, self)) self._toolbox.addTool(History(self)) self.rightWidget = QWidget() rightLayout = QGridLayout() rightLayout.setContentsMargins(0, 0, 0, 0) self.rightWidget.setLayout(rightLayout) self._mainTab = FileList(_("Subtitles"), self._subtitleData, self) self.pages = QStackedWidget(self) rightLayout.addWidget(self.pages, 0, 0) self.tabBar.addTab(self._mainTab.name) self.pages.addWidget(self._mainTab) self.splitter.addWidget(self._toolbox) self.splitter.addWidget(self.rightWidget) self.__drawSplitterHandle(1) # Setting widgets mainLayout.addWidget(self.tabBar) mainLayout.addWidget(self.splitter) # Widgets settings self.tabBar.setMovable(True) self.tabBar.setTabsClosable(True) self.tabBar.setExpanding(False) # Don't resize left panel if it's not needed leftWidgetIndex = self.splitter.indexOf(self._toolbox) rightWidgetIndex = self.splitter.indexOf(self.rightWidget) self.splitter.setStretchFactor(leftWidgetIndex, 0) self.splitter.setStretchFactor(rightWidgetIndex, 1) self.splitter.setCollapsible(leftWidgetIndex, False) self.splitter.setSizes([250]) # Some signals self.tabBar.currentChanged.connect(self.showTab) self.tabBar.tabCloseRequested.connect(self.closeTab) self.tabBar.tabMoved.connect(self.moveTab) self._mainTab.requestOpen.connect(self.openTab) self._mainTab.requestRemove.connect(self.removeFile) self.tabChanged.connect(lambda i: self._toolbox.setContentFor(self.tab(i))) self.setLayout(mainLayout) def __addTab(self, filePath): """Returns existing tab index. Creates a new one if it isn't opened and returns its index otherwise.""" for i in range(self.tabBar.count()): widget = self.pages.widget(i) if not widget.isStatic and filePath == widget.filePath: return i tab = SubtitleEditor(filePath, self._subtitleData, self) newIndex = self.tabBar.addTab(self._createTabName(tab.name, tab.history.isClean())) tab.history.cleanChanged.connect( lambda clean: self._cleanStateForFileChanged(filePath, clean)) self.pages.addWidget(tab) return newIndex def __drawSplitterHandle(self, index): splitterHandle = self.splitter.handle(index) splitterLayout = QVBoxLayout(splitterHandle) splitterLayout.setSpacing(0) splitterLayout.setContentsMargins(0, 0, 0, 0) line = QFrame(splitterHandle) line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) splitterLayout.addWidget(line) splitterHandle.setLayout(splitterLayout) def _createTabName(self, name, cleanState): if cleanState is True: return name else: return "%s +" % name def _cleanStateForFileChanged(self, filePath, cleanState): page = self.tabByPath(filePath) if page is not None: for i in range(self.tabBar.count()): if self.tabBar.tabText(i)[:len(page.name)] == page.name: self.tabBar.setTabText(i, self._createTabName(page.name, cleanState)) return def saveWidgetState(self, settings): settings.setState(self.splitter, self.splitter.saveState()) settings.setHidden(self._toolbox, self._toolbox.isHidden()) def restoreWidgetState(self, settings): self.showPanel(not settings.getHidden(self._toolbox)) splitterState = settings.getState(self.splitter) if not splitterState.isEmpty(): self.splitter.restoreState(settings.getState(self.splitter)) @pyqtSlot(str, bool) def openTab(self, filePath, background=False): if self._subtitleData.fileExists(filePath): tabIndex = self.__addTab(filePath) if background is False: self.showTab(tabIndex) else: log.error(_("SubtitleEditor not created for %s!" % filePath)) @pyqtSlot(str) def removeFile(self, filePath): tab = self.tabByPath(filePath) command = RemoveFile(filePath) if tab is not None: index = self.pages.indexOf(tab) if self.closeTab(index): self._subtitleData.execute(command) else: self._subtitleData.execute(command) @pyqtSlot(int) def closeTab(self, index): tab = self.tab(index) if tab.canClose(): widgetToRemove = self.pages.widget(index) self.tabBar.removeTab(index) self.pages.removeWidget(widgetToRemove) widgetToRemove.deleteLater() return True return False def count(self): return self.tabBar.count() def currentIndex(self): return self.tabBar.currentIndex() def currentPage(self): return self.pages.currentWidget() @pyqtSlot(int, int) def moveTab(self, fromIndex, toIndex): fromWidget = self.pages.widget(fromIndex) toWidget = self.pages.widget(toIndex) if fromWidget.isStatic or toWidget.isStatic: self.tabBar.blockSignals(True) # signals would cause infinite recursion self.tabBar.moveTab(toIndex, fromIndex) self.tabBar.blockSignals(False) return else: self.pages.removeWidget(fromWidget) self.pages.removeWidget(toWidget) if fromIndex < toIndex: self.pages.insertWidget(fromIndex, toWidget) self.pages.insertWidget(toIndex, fromWidget) else: self.pages.insertWidget(toIndex, fromWidget) self.pages.insertWidget(fromIndex, toWidget) # Hack # Qt changes tabs during mouse drag and dropping. The next line is added # to prevent it. self.showTab(self.tabBar.currentIndex()) @pyqtSlot(int) def showTab(self, index): showWidget = self.pages.widget(index) if showWidget: self.pages.setCurrentWidget(showWidget) self.tabBar.blockSignals(True) self.tabBar.setCurrentIndex(index) self.tabBar.blockSignals(False) # Try to update current tab. showWidget.updateTab() self._tabChanged.emit(index) def showPanel(self, val): if val is True: self._toolbox.show() else: self._toolbox.hide() def togglePanel(self): if self._toolbox.isHidden(): self._toolbox.show() else: self._toolbox.hide() def tab(self, index): return self.pages.widget(index) def tabByPath(self, path): for i in range(self.pages.count()): page = self.tab(i) if not page.isStatic and page.filePath == path: return page return None @property def fileList(self): return self._mainTab
class DetailsDialog(DetailsDialogBase): def __init__(self, parent, app): self.vController = None super().__init__(parent, app) def _setupUi(self): self.setWindowTitle(tr("Details")) self.resize(502, 502) self.setMinimumSize(QSize(250, 250)) self.splitter = QSplitter(Qt.Vertical) self.topFrame = EmittingFrame() self.topFrame.setFrameShape(QFrame.StyledPanel) self.horizontalLayout = QGridLayout() # Minimum width for the toolbar in the middle: self.horizontalLayout.setColumnMinimumWidth(1, 10) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setColumnStretch(0, 32) # Smaller value for the toolbar in the middle to avoid excessive resize self.horizontalLayout.setColumnStretch(1, 2) self.horizontalLayout.setColumnStretch(2, 32) # This avoids toolbar getting incorrectly partially hidden when window resizes self.horizontalLayout.setRowStretch(0, 1) self.horizontalLayout.setRowStretch(1, 24) self.horizontalLayout.setRowStretch(2, 1) self.horizontalLayout.setSpacing(1) # probably not important self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage") self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1) # Use a specific type of controller depending on the underlying viewer type self.vController = ScrollAreaController(self) self.verticalToolBar = ViewerToolBar(self, self.vController) self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical)) self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) self.referenceImageViewer = ScrollAreaImageViewer( self, "referenceImage") self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1) self.topFrame.setLayout(self.horizontalLayout) self.splitter.addWidget(self.topFrame) self.splitter.setStretchFactor(0, 8) self.tableView = DetailsTable(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) self.tableView.setSizePolicy(sizePolicy) # self.tableView.setMinimumSize(QSize(0, 190)) # self.tableView.setMaximumSize(QSize(16777215, 190)) self.tableView.setAlternatingRowColors(True) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setShowGrid(False) self.splitter.addWidget(self.tableView) self.splitter.setStretchFactor(1, 1) # Late population needed here for connections to the toolbar self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer) # self.setCentralWidget(self.splitter) # only as QMainWindow self.setWidget(self.splitter) # only as QDockWidget self.topFrame.resized.connect(self.resizeEvent) def _update(self): if self.vController is None: # Not yet constructed! return if not self.app.model.selected_dupes: # No item from the model, disable and clear everything. self.vController.resetViewersState() return dupe = self.app.model.selected_dupes[0] group = self.app.model.results.get_group_of_duplicate(dupe) ref = group.ref self.vController.updateView(ref, dupe, group) # --- Override @pyqtSlot(QResizeEvent) def resizeEvent(self, event): self.ensure_same_sizes() if self.vController is None or not self.vController.bestFit: return # Only update the scaled down pixmaps self.vController.updateBothImages() def show(self): # Give the splitter a maximum height to reach. This is assuming that # all rows below their headers have the same height self.tableView.setMaximumHeight( self.tableView.rowHeight(1) * self.tableModel.model.row_count() + self.tableView.verticalHeader().sectionSize(0) # looks like the handle is taken into account by the splitter + self.splitter.handle(1).size().height()) DetailsDialogBase.show(self) self.ensure_same_sizes() self._update() def ensure_same_sizes(self): # HACK This ensures same size while shrinking. # ReferenceViewer might be 1 pixel shorter in width # due to the toolbar in the middle keeping the same width, # so resizing in the GridLayout's engine leads to not enough space # left for the panel on the right. # This work as a QMainWindow, but doesn't work as a QDockWidget: # resize can only grow. Might need some custom sizeHint somewhere... # self.horizontalLayout.setColumnMinimumWidth( # 0, self.selectedImageViewer.size().width()) # self.horizontalLayout.setColumnMinimumWidth( # 2, self.selectedImageViewer.size().width()) # This works when expanding but it's ugly: if self.selectedImageViewer.size().width( ) > self.referenceImageViewer.size().width(): self.selectedImageViewer.resize(self.referenceImageViewer.size()) # model --> view def refresh(self): DetailsDialogBase.refresh(self) if self.isVisible(): self._update()
class CodecTab(QScrollArea): # BUG: codec_frame should have height 210 but has 480. # WORKAROUND: manually set height to 210 height. # SEE: https://forum.qt.io/topic/42055/qwidget-height-returns-incorrect-value-in-5-3/7 FRAME_HEIGHT = 210 def __init__(self, parent, context, commands): super(QWidget, self).__init__(parent) self._context = context self._logger = context.logger() self._commands = commands self._next_frame_id = 1 self._frames = QSplitter(Qt.Vertical) self._frames.setChildrenCollapsible(False) self._frames.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self._frames.setContentsMargins(0, 0, 0, 0) self._main_frame = QFrame(self) self._main_frame_layout = QVBoxLayout() self._main_frame_layout.addWidget(self._frames) self._main_frame_layout.addWidget(VSpacer(self)) self._main_frame.setLayout(self._main_frame_layout) self.newFrame("", "") self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setWidgetResizable(True) self.setWidget(self._main_frame) def newFrame(self, text, title, previous_frame=None, status=None, msg=None): try: # BUG: Setting complex default values is not possible in python # WORKAROUND: Set default value to None and set real default later. if status is None: status = StatusWidget.DEFAULT if previous_frame and previous_frame.hasNext(): next_frame = previous_frame.next() next_frame.setTitle(title) finished = False if status == StatusWidget.ERROR: while not finished: next_frame.flashStatus(status, msg) # Display error only for the first frame. msg = None finished = not next_frame.hasNext() next_frame = next_frame.next() else: next_frame.setInputText(text, msg is not None and len(msg) > 0) next_frame.flashStatus(status, msg) previous_frame.focusInputText() else: new_frame = CodecFrame(self, self._context, self._next_frame_id, self, self._commands, previous_frame, text) self._next_frame_id += 1 if self._frames.count() > 0: new_frame.flashStatus(status, msg) new_frame.setTitle(title) new_frame.setContentsMargins(0, 0, 0, 0) new_frame.layout().setContentsMargins(0, 0, 0, 0) self._frames.addWidget(new_frame) # BUG: QSplitter does not allow frames to be wider than the surrounding area (here: QScrollArea). # WORKAROUND: Set a fixed size for codec frames and disable handles which prevents users from # trying to resize the codec frames. new_frame.setFixedHeight(self.FRAME_HEIGHT) self._frames.handle(self._frames.count() - 1).setEnabled(False) if previous_frame: previous_frame.focusInputText() else: new_frame.focusInputText() except Exception as e: self._logger.error("Unknown error: {}".format(str(e))) def removeFrames(self, frame): if frame: if frame.previous(): frame.previous().setNext(None) frames_to_remove = [frame] while frame.next(): frames_to_remove.append(frame.next()) frame = frame.next() for frame_to_remove in reversed(frames_to_remove): frame_to_remove.deleteLater() def getFocussedFrame(self): widget = self._frames.focusWidget() while widget: if isinstance(widget, CodecFrame): return widget widget = widget.parent() return self._frames.widget(0)
def init_ui(self): hlay_addr = QHBoxLayout() hlay_addr.addWidget(QLabel('画廊地址')) hlay_addr.addWidget(self.ledit_addr) hlay_addr.setStretch(0, 0) hlay_cbox = QHBoxLayout() hlay_cbox.addWidget(self.cbox_rename) hlay_cbox.addWidget(self.cbox_rewrite) hlay_cbox.addStretch(1) hlay_cbox.addWidget(QLabel('从')) hlay_cbox.addWidget(self.sbox_begin_page) hlay_cbox.addWidget(QLabel('下载至')) hlay_cbox.addWidget(self.sbox_end_page) hlay_cbox.addWidget(QLabel('页')) hlay_cbox.setAlignment(Qt.AlignLeft) hlay_acts = QHBoxLayout() hlay_acts.addStretch(1) hlay_acts.addWidget(self.btn_get) hlay_acts.addWidget(self.btn_add) hlay_acts.addWidget(self.btn_remove) hlay_acts.addWidget(self.btn_start) hlay_acts.addStretch(1) vlay_left = QVBoxLayout() vlay_left.addLayout(hlay_addr) vlay_left.addLayout(hlay_cbox) vlay_left.addLayout(hlay_acts) left_wid = QWidget() left_wid.setLayout(vlay_left) vlay_right = QVBoxLayout() vlay_right.addWidget(self.user_info, alignment=Qt.AlignHCenter) vlay_right.addWidget(self.btn_refresh) vlay_right.addWidget(self.btn_logout) right_wid = QWidget() right_wid.setLayout(vlay_right) splitter = QSplitter(Qt.Horizontal) splitter.setHandleWidth(3) splitter.addWidget(left_wid) splitter.addWidget(right_wid) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 0) splitter.handle(1).setDisabled(True) vlay_info = QVBoxLayout() vlay_info.addWidget(self.thumbnail) vlay_info.addWidget(self.info) hlay_down = QHBoxLayout() hlay_down.addLayout(vlay_info) hlay_down.addWidget(self.que) vlay_main = QVBoxLayout() vlay_main.addWidget(splitter) vlay_main.addLayout(hlay_down) self.setLayout(vlay_main) self.thumbnail.setFixedHeight(left_wid.sizeHint().height()) self.thumbnail.setFixedWidth(250) self.thumbnail.setFixedHeight(360) self.thumbnail.setPixmap(self.thumb_default) if self.show_thumb_flag: # Cannot put code after show(), or flick self.thumbnail.show() else: self.thumbnail.hide()
def _setupUI(self): """ Sets up thee UI for the rearrangement Viewer. Notes: helper method for __init__ Returns: None """ self.scrollA = QScrollArea() self.listA = SourceList(self) self.listA.itemSelectionChanged.connect(self.show_relevant_tools) self.scrollA.setWidget(self.listA) self.scrollA.setWidgetResizable(True) self.scrollB = QScrollArea() self.listB = SinkList(self) self.listB.itemSelectionChanged.connect(self.show_relevant_tools) self.scrollB.setWidget(self.listB) self.scrollB.setWidgetResizable(True) self.appendB = QToolButton() # TODO: move &A here and use alt-Enter to Accept dialog? self.appendB.setText("Add &Page(s)") self.appendB.setArrowType(Qt.DownArrow) self.appendB.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.removeB = QToolButton() self.removeB.setArrowType(Qt.UpArrow) self.removeB.setText("&Remove Page(s)") self.removeB.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.sLeftB = QToolButton() self.sLeftB.setArrowType(Qt.LeftArrow) self.sLeftB.setText("Shift Left") self.sLeftB.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.sRightB = QToolButton() self.sRightB.setArrowType(Qt.RightArrow) self.sRightB.setText("Shift Right") self.sRightB.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.reverseB = QPushButton("Reverse Order") self.revertB = QPushButton("Revert to original state") self.revertB.clicked.connect(self.populateListOriginal) self.rotateB_cw = QPushButton("\N{Clockwise Open Circle Arrow} Rotate CW") self.rotateB_ccw = QPushButton("\N{Anticlockwise Open Circle Arrow} Rotate CCW") self.closeB = QPushButton("&Cancel") self.acceptB = QPushButton("&Accept") self.permute = [False] def GrippyMcGrab(): """Grippy bars to spice-up QSplitterHandles.""" width = 64 pad = 20 hb = QHBoxLayout() hb.addItem( QSpacerItem(pad, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) ) vb = QVBoxLayout() hb.addLayout(vb) hb.addItem( QSpacerItem(pad, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) ) vb.setContentsMargins(0, 1, 0, 1) vb.setSpacing(2) vb.addItem( QSpacerItem( width, 3, QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding ) ) for i in range(3): f = QFrame() f.setFrameShape(QFrame.HLine) f.setFrameShadow(QFrame.Sunken) vb.addWidget(f) vb.addItem( QSpacerItem( width, 3, QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding ) ) return hb hb3 = QHBoxLayout() self.tools = QFrame() hb = QHBoxLayout() self.tools.setLayout(hb) hb.setContentsMargins(0, 0, 0, 0) hb.addWidget(self.rotateB_ccw) hb.addWidget(self.rotateB_cw) hb.addItem(QSpacerItem(16, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)) hb.addWidget(self.sLeftB) hb.addWidget(self.sRightB) hb.addItem(QSpacerItem(16, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)) hb3.addWidget(self.tools) hb3.addWidget(self.reverseB) hb3.addItem( QSpacerItem(16, 20, QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) ) hb3.addWidget(self.acceptB) hb3.addWidget(self.closeB) allPages = QLabel("Other Pages in Exam") thisQuestion = QLabel("Pages for this Question") # center add/remove buttons on label row hb1 = QHBoxLayout() hb1.addWidget(thisQuestion) hb1.addLayout(GrippyMcGrab()) hb = QHBoxLayout() hb.addWidget(self.appendB) hb.addItem(QSpacerItem(64, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)) hb.addWidget(self.removeB) hb1.addLayout(hb) hb1.addLayout(GrippyMcGrab()) hb1.addWidget(self.revertB) vb0 = QVBoxLayout() s = QSplitter() s.setOrientation(Qt.Vertical) # s.setOpaqueResize(False) s.setChildrenCollapsible(False) s.setHandleWidth(50) # TODO: better not to hardcode, take from children? vb0.addWidget(s) f = QFrame() s.addWidget(f) vb = QVBoxLayout() vb.setContentsMargins(0, 0, 0, 0) f.setLayout(vb) vb.addWidget(allPages) vb.addWidget(self.scrollA) f = QFrame() s.addWidget(f) vb = QVBoxLayout() vb.setContentsMargins(0, 0, 0, 0) f.setLayout(vb) vb.addWidget(self.scrollB) vb.addLayout(hb3) handle = s.handle(1) vb = QVBoxLayout() vb.setContentsMargins(0, 0, 0, 0) vb.setSpacing(0) handle.setLayout(hb1) hb1.setContentsMargins(0, 0, 0, 0) # TODO: Buttons inside the splitter bar, disable drag and custom cursor for b in (self.removeB, self.appendB, self.revertB): b.mouseMoveEvent = lambda *args: None b.setCursor(Qt.ArrowCursor) self.setLayout(vb0) self.resize(QSize(self.parent.width() * 7 / 8, self.parent.height() * 9 / 10)) self.closeB.clicked.connect(self.close) self.sLeftB.clicked.connect(self.shuffleLeft) self.sRightB.clicked.connect(self.shuffleRight) self.reverseB.clicked.connect(self.reverseOrder) self.rotateB_cw.clicked.connect(lambda: self.rotateImages(90)) self.rotateB_ccw.clicked.connect(lambda: self.rotateImages(-90)) self.appendB.clicked.connect(self.sourceToSink) self.removeB.clicked.connect(self.sinkToSource) self.acceptB.clicked.connect(self.doShuffle) allPageWidgets = [self.listA, self.listB] self.listA.selectionModel().selectionChanged.connect( lambda sel, unsel: self.singleSelect(self.listA, allPageWidgets) ) self.listB.selectionModel().selectionChanged.connect( lambda sel, unsel: self.singleSelect(self.listB, allPageWidgets) )
class ClientWindow(QMainWindow): """GUI主窗口类。""" def __init__(self): super().__init__() self.files = [] # 发送端待发送文件列表 self.del_list = [] # 发送端待删除文件列表 self.received_files = 0 # 接收端已接受文件数 self.succeed_files = 0 # 接收端成功接受文件数 self.failed_files = 0 # 接收端传输失败文件数 self.client_que = Queue() self.server_que = Queue() self.setFont(QFont('Arial', 10)) self.resolution = QGuiApplication.primaryScreen().availableGeometry() self.reso_height = self.resolution.height() self.reso_width = self.resolution.width() self.settings = QSettings(os.path.join(os.path.abspath('.'), 'settings.ini'), QSettings.IniFormat) self.settings.beginGroup('UISetting') self.setting_detail_view = int(self.settings.value('detail_view', False)) self.settings.endGroup() self.settings.beginGroup('ClientSetting') self.setting_del_timeout = float(self.settings.value('del_timeout', 1)) self.settings.endGroup() self.init_ui() """UI构造函数""" def init_ui(self): self.sender_frame = QFrame() self.chat_frame = QFrame() self.splitter = QSplitter(Qt.Horizontal) self.splitter.setHandleWidth(self.reso_width / 384) # 拖动杆宽度 self.splitter.splitterMoved.connect(self.resizeEvent) self.splitter.addWidget(self.sender_frame) self.splitter.addWidget(self.chat_frame) self.splitter.handle(1).setStyleSheet('QSplitterHandle{background-color: rgb(210,210,210)}') self.setCentralWidget(self.splitter) # 将分隔控件作为窗口主控件 # 菜单栏定义 menu_bar = self.menuBar() file_menu = menu_bar.addMenu('文件(&F)') self.act_choose = QAction('选择文件(&C)', self) self.act_send = QAction('发送(&S)', self) self.act_stop_send = QAction('中止传输(&E)', self) self.act_exit = QAction('退出(&Q)', self) file_menu.addAction(self.act_choose) file_menu.addAction(self.act_send) file_menu.addAction(self.act_stop_send) file_menu.addSeparator() file_menu.addAction(self.act_exit) self.act_choose.triggered.connect(self.file_dialog) self.act_send.setDisabled(True) self.act_send.triggered.connect(self.file_checker) self.act_stop_send.setDisabled(True) self.act_stop_send.triggered.connect(self.abort_trans) self.act_exit.triggered.connect(self.close) setting_menu = menu_bar.addMenu('设置(&S)') act_client = QAction('发送端设置(&C)', self) act_server = QAction('接收端设置(&S)', self) act_ui = QAction('界面设置(&U)', self) setting_menu.addAction(act_client) setting_menu.addAction(act_server) setting_menu.addAction(act_ui) act_client.triggered.connect(self.client_setting_dialog) act_server.triggered.connect(self.server_setting_dialog) act_ui.triggered.connect(self.ui_setting_dialog) # 状态栏定义 self.status_bar = self.statusBar() self.status_bar.setSizeGripEnabled(False) self.Lclient_status = QLabel('发送端已就绪') self.Lserver_status = QLabel('接收端未启动') self.status_bar.addWidget(self.Lclient_status, 1) self.status_bar.addWidget(self.Lserver_status, 1) # 传输区域控件定义 self.Bselector = QPushButton('选择文件', self) self.Bselector.setDefault(True) self.Bselector.clicked.connect(self.file_dialog) self.Lfile_empty = QLabel('未选中文件') self.Lfile_empty.setAlignment(Qt.AlignTop) self.file_table = QTableWidget() # 文件列表采用表格视图 self.file_table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.file_table.setSelectionMode(QAbstractItemView.NoSelection) self.file_table.verticalScrollBar().setStyleSheet('QScrollBar{{width:{}px;}}'.format(self.reso_width / 192)) self.file_table.verticalScrollBar().setContextMenuPolicy(Qt.NoContextMenu) self.file_table.verticalHeader().setSectionsClickable(False) self.file_table.horizontalHeader().setSectionsClickable(False) self.file_table.hide() self.Bfile_sender = QPushButton('发送', self) self.Bfile_sender.setDisabled(True) self.Bfile_sender.clicked.connect(self.file_checker) self.vbox = QVBoxLayout() self.vbox.setSpacing(self.reso_height / 70) self.vbox.addWidget(self.Bselector) self.vbox.addWidget(self.file_table) self.vbox.addWidget(self.Lfile_empty) self.vbox.addWidget(self.Bfile_sender) self.sender_frame.setLayout(self.vbox) # 消息区域控件定义 self.Emessage_area = def_widget.MessageDisplayEdit(self) self.Emessage_area.setContextMenuPolicy(Qt.DefaultContextMenu) self.Emessage_area.setReadOnly(True) self.Emessage_writer = def_widget.MessageWriter(self) self.Emessage_writer.setPlaceholderText('输入消息') self.Emessage_writer.returnPressed.connect(self.chat_checker) self.Bfolder = QPushButton('<< 收起', self) self.Bfolder.clicked.connect(functools.partial(self.splitter.setSizes, [self.geometry().width(), 0])) self.Bmessage_sender = QPushButton('发送', self) self.Bmessage_sender.clicked.connect(self.chat_checker) self.chat_vbox = QVBoxLayout() self.chat_vbox.setSpacing(self.reso_height / 70) self.chat_vbox.addWidget(self.Emessage_area) self.chat_vbox.addWidget(self.Emessage_writer) self.chat_hbox = QHBoxLayout() self.chat_hbox.addWidget(self.Bfolder) self.chat_hbox.addStretch(1) self.chat_hbox.addWidget(self.Bmessage_sender) self.chat_vbox.addLayout(self.chat_hbox) self.chat_frame.setLayout(self.chat_vbox) if not self.setting_detail_view: # 显示模式不同,最小宽度不同 self.sender_frame.setMinimumWidth(self.reso_width / 7.68) else: self.sender_frame.setMinimumWidth(self.reso_width / 6) self.ui_setting_checker() self.settings.beginGroup('Misc') # 恢复上次关闭时的窗口尺寸 setting_window_size = self.settings.value('window_size', (self.reso_height / 6.4, self.reso_width / 2.7)) setting_frame_width = self.settings.value('frame_width', (self.geometry().width(), 0)) self.settings.endGroup() self.resize(setting_window_size[0], setting_window_size[1]) self.splitter.setSizes([setting_frame_width[0], setting_frame_width[1]]) self.frameGeometry().moveCenter(self.resolution.center()) # 在屏幕中央打开窗口 self.setWindowTitle('FileTransfer') self.show() self.settings.beginGroup('ServerSetting') setting_open_server = int(self.settings.value('open_server', True)) self.settings.endGroup() if setting_open_server: # 启动服务端 self.settings.beginGroup('ServerSetting') setting_incoming_ip = self.settings.value('incoming_ip', '0.0.0.0') setting_bind_port = int(self.settings.value('bind_port', 54321)) setting_receive_dir = self.settings.value('receive_dir', os.path.abspath('.')) self.settings.endGroup() self.server_starter = Process(target=server.starter, name='ServerStarter', args=( setting_incoming_ip, setting_bind_port, setting_receive_dir, self.server_que)) self.server_starter.start() self.server_timer = QTimer() # 服务端消息读取循环 self.server_timer.timeout.connect(self.server_status) self.server_timer.start(200) def simple_viewer(self, add_files): """简明视图:仅包含按钮及进度条。""" # 简明视图样式:无框线,水平抬头不可见,按钮定宽,文件名列宽自适应留白 self.file_table.setColumnCount(2) row = len(self.files) + len(add_files) self.file_table.setRowCount(row) self.file_table.setShowGrid(False) self.file_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.file_table.horizontalHeader().setVisible(False) self.file_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.file_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) num = 0 for inst in add_files: prog_widget = QWidget() prog_stack = QStackedLayout() prog_stack.addWidget(inst.prog) prog_stack.addWidget(inst.label) prog_stack.setStackingMode(QStackedLayout.StackAll) prog_widget.setLayout(prog_stack) inst.button.clicked.connect(functools.partial(self.del_file, inst)) inst.button.pressed.connect(self.button_pressed) inst.button.released.connect(self.button_released) index = num + len(self.files) num += 1 self.file_table.setCellWidget(index, 0, inst.button) self.file_table.setCellWidget(index, 1, prog_widget) self.files += add_files self.file_table.show() for inst in self.files: # 列表显示后再截断文件名 inst.label.setText(self.shorten_filename(inst.name, self.file_table.columnWidth(1))) def detail_viewer(self, add_files): """详细视图:包括按钮、进度条、详细进度、文件大小、状态""" self.file_table.setColumnCount(5) row = len(self.files) + len(add_files) self.file_table.setRowCount(row) self.file_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.file_table.setHorizontalHeaderLabels(['', '文件名', '传输进度', '文件大小', '状态']) # 要用表头的ResizeMode函数而不能用列的ResizeMode函数,否则表头仍可拖动 self.file_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.file_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) num = 0 for inst in add_files: prog_stack = QStackedLayout() prog_stack.addWidget(inst.prog) prog_stack.addWidget(inst.label) prog_stack.setStackingMode(QStackedLayout.StackAll) prog_widget = QWidget() prog_widget.setLayout(prog_stack) # 定义按钮点击删除,长按清空的行为 inst.button.clicked.connect(functools.partial(self.del_file, inst)) inst.button.pressed.connect(self.button_pressed) inst.button.released.connect(self.button_released) file_prog = QTableWidgetItem('0.00 %') file_prog.setTextAlignment(Qt.AlignCenter) file_size = QTableWidgetItem(inst.size) file_size.setTextAlignment(Qt.AlignCenter) file_status = QTableWidgetItem(inst.status[1]) file_status.setTextAlignment(Qt.AlignCenter) index = num + len(self.files) num += 1 self.file_table.setCellWidget(index, 0, inst.button) self.file_table.setCellWidget(index, 1, prog_widget) self.file_table.setItem(index, 2, file_prog) self.file_table.setItem(index, 3, file_size) self.file_table.setItem(index, 4, file_status) self.files += add_files self.file_table.show() row_height = self.file_table.rowHeight(0) * self.file_table.rowCount() header_height = self.file_table.horizontalHeader().height() if row_height + header_height >= self.file_table.height(): # 计算是否出现滚动条 for inst in self.files: changed_text = self.shorten_filename(inst.name, self.file_table.columnWidth(1) - self.reso_width / 192) inst.label.setText(changed_text) else: for inst in self.files: changed_text = self.shorten_filename(inst.name, self.file_table.columnWidth(1)) inst.label.setText(changed_text) def ui_sending(self): """将UI转变为传输中状态。""" self.Bfile_sender.setText('中止传输') self.Bfile_sender.clicked.disconnect(self.file_checker) self.Bfile_sender.clicked.connect(self.abort_trans) self.act_choose.setDisabled(True) self.act_send.setDisabled(True) self.act_stop_send.setDisabled(False) self.Bselector.setDisabled(True) for inst in self.files: # 禁用删除按钮 inst.button.setDisabled(True) def ui_pending(self): """将UI转变为等待中状态。""" self.Bfile_sender.setText('发送') self.Bfile_sender.clicked.disconnect(self.abort_trans) self.Bfile_sender.clicked.connect(self.file_checker) self.act_choose.setDisabled(False) self.act_stop_send.setDisabled(True) self.Bselector.setDisabled(False) for inst in self.files: # 启用删除按钮 inst.button.setDisabled(False) def ui_setting_checker(self): """UI设置窗口关闭后转变UI""" self.settings.beginGroup('UISetting') setting_status_bar = int(self.settings.value('status_bar', True)) setting_chat_frame = int(self.settings.value('chat_frame', True)) self.settings.endGroup() if setting_status_bar: self.status_bar.show() else: self.status_bar.hide() if setting_chat_frame: self.chat_frame.show() else: self.chat_frame.hide() def client_setting_checker(self): """客户端设置窗口关闭后转变删除延时""" self.settings.beginGroup('ClientSetting') self.setting_del_timeout = float(self.settings.value('del_timeout', 1)) self.settings.endGroup() """进程启动函数""" def chat_checker(self): """聊天进程启动函数。""" text = self.Emessage_writer.text() if text: self.settings.beginGroup('ClientSetting') setting_host = self.settings.value('host', '127.0.0.1') setting_port = int(self.settings.value('server_port', 12345)) self.settings.endGroup() try: if self.chat_sender.is_alive(): # 若上个消息没发完就发新消息则结束掉上个子进程 self.chat_sender.terminate() self.chat_sender.join() self.Emessage_area.moveCursor(QTextCursor.End) self.Emessage_area.insertHtml('<font color=red>✘<font color=black> ') except AttributeError: pass self.Emessage_area.append('本机(localhost):\n {0} '.format(text)) self.chat_sender = Process(target=chat_client.chat_starter, name='ChatSender', args=(setting_host, setting_port, text, self.server_que)) self.chat_sender.start() self.Emessage_writer.clear() def file_checker(self): """文件列表构建及客户端传输进程开启。""" self.ui_sending() self.settings.beginGroup('ClientSetting') setting_file_at_same_time = int(self.settings.value('file_at_same_time', 2)) setting_host = self.settings.value('host', '127.0.0.1') setting_port = int(self.settings.value('server_port', 12345)) self.settings.endGroup() for inst in self.files: inst.status = 'uploading' inst.prog.setValue(0) index = self.find_index_by_name(inst.name) self.file_table.item(index, 4).setText('传输中') path_list = [inst.path for inst in self.files] try: self.file_sender = Process(target=trans_client.starter, name='FileSender', args=( setting_host, setting_port, path_list, setting_file_at_same_time, self.client_que)) self.file_sender.start() self.Lclient_status.setText( '''传输中:<font color=green>0<font color=black>/<font color=red>0<font color=black>/{0} (<font color=green>Comp<font color=black>/<font color=red>Err<font color=black>/Up)'''.format( len(self.find_instance_by_status('uploading')))) except Exception as e: self.Lclient_status.setText(repr(e)) self.prog_timer = QTimer() # 启动进度条更新监控 self.prog_timer.timeout.connect(self.update_prog) self.prog_timer.start(5) """后台监控函数""" def server_status(self): """启动时循环读取服务端状态及更新聊天信息。""" try: message = self.server_que.get(block=False) if message['type'] == 'server_info': if message['message'] == 'error': self.Lserver_status.setText(message['detail']) elif message['message'] == 'ready': self.Lserver_status.setText('接收端已就绪') elif message['message'] == 'MD5_passed': self.received_files += 1 self.succeed_files += 1 self.Lserver_status.setText( '''{0}=<font color=green>{1}<font color=black>+<font color=red>{2}<font color=black> (Tol=<font color=green>Comp<font color=black>+<font color=red>Err<font color=black>)'''.format( self.received_files, self.succeed_files, self.failed_files)) elif message['message'] == 'MD5_failed': # 失败数统计 self.received_files += 1 self.failed_files += 1 self.Lserver_status.setText( '''{0}=<font color=green>{1}<font color=black>+<font color=red>{2}<font color=black> (Tol=<font color=green>Comp<font color=black>+<font color=red>Err<font color=black>)'''.format( self.received_files, self.succeed_files, self.failed_files)) elif message['message'] == 'started': # 正在传输数统计 self.Lserver_status.setText( '''新文件传入 {0}=<font color=green>{1}<font color=black>+<font color=red>{2}<font color=black> (Tol=<font color=green>Comp<font color=black>+<font color=red>Err<font color=black>)'''.format( self.received_files, self.succeed_files, self.failed_files)) elif message['message'] == 'aborted': # 中断数统计 self.Lserver_status.setText( '''传输中断 {0}=<font color=green>{1}<font color=black>+<font color=red>{2}<font color=black> (Tol=<font color=green>Comp<font color=black>+<font color=red>Err<font color=black>)'''.format( self.received_files, self.succeed_files, self.failed_files)) elif message['type'] == 'chat': if message['status'] == 'success': self.chat_sender.terminate() self.chat_sender.join() self.Emessage_area.moveCursor(QTextCursor.End) self.Emessage_area.insertHtml('<font color=green>✓<font color=black> ') elif message['status'] == 'failed': self.chat_sender.terminate() self.chat_sender.join() self.Emessage_area.moveCursor(QTextCursor.End) self.Emessage_area.insertHtml('<font color=red>✘<font color=black> ') elif message['status'] == 'received': self.Emessage_area.append( '{0}:{1}:\n {2}'.format(message['from'][0], message['from'][1], message['message'])) self.Lserver_status.setText('收到来自{0}:{1}的聊天消息'.format(message['from'][0], message['from'][1])) except queue.Empty: pass def update_prog(self): """传输过程管理及进度条更新函数。""" try: message = self.client_que.get(block=False) inst = self.find_instance_by_name(message['name']) # 考虑到简明视图的性能,不全局计算index if message['type'] == 'info': if message['message'] == 'MD5_passed': inst.status = 'complete' index = self.find_index_by_name(message['name']) self.file_table.item(index, 4).setText('传输完成') self.file_table.item(index, 2).setText('100 %') self.del_list.append(inst.path) elif message['message'] == 'MD5_failed': inst.status = 'error' index = self.find_index_by_name(message['name']) self.file_table.item(index, 4).setText('传输错误') self.file_table.item(index, 2).setText('100 %') elif message['message'] == 'aborted': for inst in self.find_instance_by_status('uploading'): inst.status = 'error' i = self.find_index_by_name(inst.name) self.file_table.item(i, 4).setText('传输中断') self.ui_pending() self.prog_timer.stop() # 若无上传中的文件且未被中断,关闭传输进程,清空上传列表且提示完成结果 if not self.find_instance_by_status('uploading') and self.prog_timer.isActive(): self.Lclient_status.setText( '''传输完成:<font color=green>{0}<font color=black>/<font color=red>{1}<font color=black>/0 (<font color=green>Comp<font color=black>/<font color=red>Err<font color=black>/Up)'''.format( len(self.find_instance_by_status('complete')), len(self.find_instance_by_status('error')))) self.file_sender.terminate() self.file_sender.join() self.file_sender.close() self.prog_timer.stop() if self.del_list and not self.find_instance_by_status('error'): # 待删除列表不为空且没有传输错误则考虑是否删除源文件 self.settings.beginGroup('ClientSetting') setting_del_source = int(self.settings.value('del_source', False)) self.settings.endGroup() if setting_del_source: msg_box = QMessageBox(self) msg_box.setWindowTitle('成功') msg_box.setIcon(QMessageBox.Information) msg_box.setText('传输成功完成!\n点击确定删除源文件') msg_box.addButton('确定', QMessageBox.AcceptRole) msg_box.addButton('取消', QMessageBox.DestructiveRole) reply = msg_box.exec() if reply == QMessageBox.AcceptRole: for path in self.del_list: try: os.remove(path) except Exception as e: msg_box = QMessageBox(self) msg_box.setWindowTitle('错误') msg_box.setIcon(QMessageBox.Critical) msg_box.setInformativeText('无法删除\n{0}'.format(path)) msg_box.setText(repr(e)) msg_box.addButton('确定', QMessageBox.AcceptRole) msg_box.exec() self.remove_all() else: msg_box = QMessageBox(self) msg_box.setWindowTitle('成功') msg_box.setIcon(QMessageBox.Information) msg_box.setText('传输成功完成!') msg_box.addButton('确定', QMessageBox.AcceptRole) msg_box.exec() else: msg_box = QMessageBox(self) msg_box.setWindowTitle('警告') msg_box.setIcon(QMessageBox.Warning) msg_box.setText('传输完成,但有文件传输出错!') msg_box.addButton('确定', QMessageBox.AcceptRole) msg_box.exec() self.ui_pending() else: self.Lclient_status.setText( '''传输中:<font color=green>{0}<font color=black>/<font color=red>{1}<font color=black>/{2} (<font color=green>Comp<font color=black>/<font color=red>Err<font color=black>/Up)'''.format( len(self.find_instance_by_status('complete')), len(self.find_instance_by_status('error')), len(self.find_instance_by_status('uploading')))) elif message['type'] == 'prog': inst.prog.setValue(message['part'] + 1) if self.setting_detail_view: # 详细视图:更新进度百分比 index = self.find_index_by_name(message['name']) file_prog = message['part'] / inst.prog.maximum() * 100 self.file_table.item(index, 2).setText('{0:.2f} %'.format(file_prog)) except queue.Empty: pass """绑定事件函数""" def del_file(self, inst): """按钮绑定删除事件:从当前表格中找到行索引并删除以及删除文件列表中的实例。""" index = self.find_index_by_name(inst.name) self.file_table.removeRow(index) self.files.remove(inst) if not self.files: self.file_table.hide() self.Lfile_empty.show() self.act_send.setDisabled(True) self.Bfile_sender.setDisabled(True) def remove_all(self): for i in range(len(self.files), 0, -1): # 批量删除时需要从末尾开始逐个删除 self.del_file(self.files[i - 1]) def button_pressed(self): self.button_timer = QTimer() self.button_timer.timeout.connect(self.remove_all) self.button_timer.setSingleShot(True) self.button_timer.start(self.setting_del_timeout * 1000) def button_released(self): self.button_timer.stop() """通用功能性函数""" def abort_trans(self): """中断传输函数:发送取消消息""" self.settings.beginGroup('ClientSetting') setting_host = self.settings.value('host', '127.0.0.1') setting_port = int(self.settings.value('server_port', 12345)) self.settings.endGroup() try: self.file_sender.terminate() # 关闭当前的发送进程 self.file_sender.join() self.file_sender.close() del self.file_sender except ValueError: pass except AttributeError: pass self.abort_sender = Process(target=trans_client.starter, name='AbortSender', args=(setting_host, setting_port, '', None, self.client_que)) self.abort_sender.start() self.abort_sender.join(5) if self.abort_sender.exitcode is None: # join超时,未收到中断回包时 msg_box = QMessageBox(self) msg_box.setWindowTitle('警告') msg_box.setIcon(QMessageBox.Warning) msg_box.setText('接收端无响应!') msg_box.addButton('确定', QMessageBox.AcceptRole) msg_box.exec() self.abort_sender.kill() for inst in self.find_instance_by_status('uploading'): inst.status = 'error' i = self.find_index_by_name(inst.name) self.file_table.item(i, 4).setText('传输中断') self.ui_pending() self.abort_sender.join() self.abort_sender.close() del self.abort_sender def shorten_filename(self, name, width): """根据给定的宽度截断文件名并添加...,返回截断后的文件名。""" metrics = QFontMetrics(self.font()) if metrics.width(name) > width - self.reso_width / 128: for i in range(4, len(name)): # 从第4个字符开始计算长度 if metrics.width(name[:i]) > width - self.reso_width / 128: return name[:i] + '...' return name def safe_close(self): """结束各个子进程及计时器,做好退出准备。""" self.settings.beginGroup('Misc') # 记录关闭时的窗口尺寸 self.settings.setValue('window_size', (self.geometry().width(), self.geometry().height())) self.settings.setValue('frame_width', (self.sender_frame.width(), self.chat_frame.width())) self.settings.endGroup() self.settings.sync() try: # 关闭后台监控进程和传输子进程 self.server_timer.stop() del self.server_timer self.server_starter.terminate() self.server_starter.join() self.server_starter.close() self.prog_timer.stop() del self.prog_timer self.file_sender.terminate() self.file_sender.join() self.file_sender.close() except ValueError: # 忽略传输子进程已关闭时的异常 pass except AttributeError: # 忽略还未声明变量时的异常 pass try: # 关闭聊天子进程 self.chat_sender.terminate() self.chat_sender.join() self.chat_sender.close() except ValueError: pass except AttributeError: pass def find_instance_by_name(self, name): """按文件名在文件列表中查找实例,没有返回None。""" for inst in self.files: if inst.name == name: return inst return None def find_instance_by_status(self, status): """按文件状态在文件列表中查找实例,没有返回空列表。""" inst_list = [inst for inst in self.files if inst.status[0] == status] return inst_list def find_index_by_name(self, name): """按文件名在表格视图中查找索引,没有返回None。""" row = self.file_table.rowCount() for i in range(row): if self.file_table.cellWidget(i, 1).layout().widget(1).toolTip() == name: return i return None """对话框实例创建函数""" def file_dialog(self): """文件选择对话框,制作文件实例。""" self.settings.beginGroup('Misc') setting_path_history = self.settings.value('path_history', '.') self.settings.endGroup() fname = QFileDialog.getOpenFileNames(self, '请选择文件', setting_path_history) if fname[0]: new_list = set(fname[0]) old_list = {inst.path for inst in self.files} old_name = {inst.name for inst in self.files} same_list = {path for path in new_list if os.path.split(path)[1] in old_name} add_list = new_list - old_list - same_list # 用集合set求纯新增文件路径列表(剔除重名项) add_files = [FileStatus(path) for path in add_list] self.Lfile_empty.hide() if not self.setting_detail_view: self.simple_viewer(add_files) else: self.detail_viewer(add_files) self.Bfile_sender.setDisabled(False) self.act_send.setDisabled(False) self.settings.beginGroup('Misc') self.settings.setValue('path_history', os.path.split(fname[0][-1])[0]) self.settings.endGroup() self.settings.sync() def client_setting_dialog(self): self.client_setting = def_widget.ClientSettingDialog(self) self.client_setting.setAttribute(Qt.WA_DeleteOnClose) self.client_setting.destroyed.connect(self.client_setting_checker) self.client_setting.show() def server_setting_dialog(self): self.server_setting = def_widget.ServerSettingDialog(self) self.server_setting.setAttribute(Qt.WA_DeleteOnClose) self.server_setting.show() def ui_setting_dialog(self): self.ui_setting = def_widget.UIDialog(self) self.ui_setting.setAttribute(Qt.WA_DeleteOnClose) self.ui_setting.destroyed.connect(self.ui_setting_checker) self.ui_setting.show() """事件函数重载""" def closeEvent(self, event): """关闭事件函数:检查是否有传输中的文件并退出""" if self.find_instance_by_status('uploading'): msg_box = QMessageBox(self) msg_box.setWindowTitle('警告') msg_box.setIcon(QMessageBox.Warning) msg_box.setText('文件传输中,是否中断并退出?') msg_box.addButton('确定', QMessageBox.AcceptRole) msg_box.addButton('取消', QMessageBox.DestructiveRole) reply = msg_box.exec() if reply == QMessageBox.AcceptRole: self.abort_trans() self.safe_close() event.accept() else: event.ignore() else: self.safe_close() event.accept() def keyPressEvent(self, k): """按ESC时触发的关闭行为。""" if k.key() == Qt.Key_Escape: self.close() def resizeEvent(self, event): """调整窗口尺寸时触发。""" for inst in self.files: # 根据表格列宽调整截断文件名 changed_text = self.shorten_filename(inst.name, self.file_table.columnWidth(1)) inst.label.setText(changed_text)