====== Python and Qt - The Story That Gets Complex ====== * **If you don't want read or already read all below, The Story all can be simplified into my Python Qt universal_tool_template.py** since 2016.07 * my internal wiki link (slight slow to update): [[devwiki:template:universal_tool_template|universal_tool_template]] * my external github link (more updated): https://github.com/shiningdesign/universal_tool_template.py * Qt is a GUI library, which almost any programming language can use, default designed for C++ * Qt has currently 2 active version: - Qt4: old and stable - Qt5: new and code structure changes * Python is programming language, default can also run in console command mode, bundle with tk GUI that no one use * Python has current 2 major active version: Python 2.x and Python 3.x; which popular version are - Python 2.7 32bit: (version I use for my own apps) - Python 2.7 64bit: (version used by Maya2014 64bit, Maya2017 64bit, Houdini v15 64bit, Nuke 10 64bit) - Python 3.4 32bit - Python 3.4 64bit - Python 3.5.1 32bit - Python 3.5.1 64bit: (version used by Blender 2.78) * Python to Qt need a binding library, which let python to call those Qt library to create GUI * Python has currently 2 major active binding library: PySide, PyQt, which popular version are - PySide: python + Qt4 - PySide2: Python + Qt5 - PyQt4: python + Qt4 - PyQt5: python + Qt5 * **The Selection Table by use case**: * However, you can manually setup the library and use in any combination with match pyVersion+osVersion+qtVersion. * You don't need get Qt4 or Qt5 package seperately, as PySide,PySide2,PyQt4,PyQt5 all have Qt library included * **IMPORTANT NOTE: if you manually install PySide, PyQt library into Python, make sure the package version matching your Python Major+Sub version + Bit choice, like package for extactly py35x64 ** ^ Python version and bit ^^ Qt binding ^^ Default Used by ^ Install Options | ^ Python 2.7 | 32bit ^ PyQt4 | Qt4 | my personal tools | https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.11.4/ | ^ Python 2.7 | 64bit ^ PySide | Qt4 | Maya2014 64bit, Houdini v15 64bit, Nuke 10 64bit | ^ Python 2.7 | 64bit ^ PySide2 | Qt5 | Maya2017 64bit | ^ Python 3.4 | 64bit ^ PyQt4 | Qt4 | | https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.11.4/ | ^ Python 3.4 | 64bit ^ PyQt5 | Qt5 | | pip3 install PyQt5 | ^ Python 3.5 | 64bit ^ PyQt4 | Qt4 | | http://www.lfd.uci.edu/%7Egohlke/pythonlibs/#pyqt4 | ^ Python 3.5 | 64bit ^ PyQt5 | Qt5 | | pip3 install PyQt5 \\ whl https://pypi.python.org/pypi/PyQt5 | * note: pip.exe, pip3.exe are in python's script folder, run OS command console * install from a whl using pip: pip install some-package.whl * pyside download by pip: https://pypi.python.org/pypi/PySide * pyside2 download: https://wiki.qt.io/PySide2 * **Quick Conclusion from observe** * Python 2.7 is most popular choice for software script integration, for its stable and no change in long time * Python 64bit is best choice for modern computer, since all PC got more than 16GB ram nowadays * PySide (Qt4 inside) is most popular choice for Commercial software qt integration, for its easy-to-use LGPL license, and also for its stable Qt4 library * However, * Python 3.5 64bit is for open mind modern Development, since it is newest * PyQt4 and PyQt5 license more friendly to open source and internal projects * PySide and PySide2 license are friendly to both commercial and open source use, but documentation may not be on PyQt4,PyQt5 level * Qt4 library is old and stable like Python 2.7 * Qt5 library is new and more for people willing to adapt ===== Install a PythonQt binding into existing software is may be straight forward ===== * those PySide, PyQt binding are built with a target, if they are built for the operation system and python version ==== Qt for Blender ==== * Same case for Blender 2.7.8, since it comes no pyside or pyqt, you have build/find a version for targeted Blender, and you **May** just grab standard python 3.5.1's PyQt library and throw the path inside. * Blender target version of PyQt4 download: http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyqt4 * install PyQt4 for normal Python 3.5 64bit, copy the PyQt4 folder and SIP.pyd to blenderApp\2.78\python\lib\site-packages * run python console in Blender, it works. ==== Qt for Maya ==== * PySide is with Maya by defaut, you also can get PyQt4 for Maya from here: * Maya target version of PyQt4 download: * PyQt4 for Maya 2012 - 2016 (win,mac): https://github.com/zclongpop123/PyQt4 * PyQt5 for maya 2017 (win): https://github.com/zclongpop123/PyQt5-for-Maya2017 * Qt for Maya tutorial: http://animateshmanimate.com/2010/07/19/maya-and-qt-tutorial/ * Quick Qt button commander to Maya: http://www.janpijpers.com/qt-and-maya-how-to-use-a-qt-designer-ui-with-maya-no-pyqt/ * similar guide: http://www.creativecrash.com/tutorials/using-qt-designer-for-mel-interfaces#tabs Quick notes: * loadUI mel command is for loading Qt UI file, and if detect maya compatible UI element, it auto generate maya UI element instead, like button with -c flag. * pyQt method is directly generating Qt element, but mixing Qt and Maya element is possible through hard way: example: http://www.justinfx.com/2011/11/20/mixing-pyqt4-widgets-and-maya-ui-objects/ ^ Qt UI ^ Maya UI ^ | push button (Buttons group) | button | | radio button (Buttons group) | radioButton | | check box(Buttons group) | checkBox | | combo box (containers group) | optionMenu | | line edit (input widgets group) | textField | | spin box (input widgets group) | NONE | | double spine box (input widgets group)| NONE | | dial (input widgets group) | NONE | | list widget (item widget item based) | textScrollList | | horizontal slider (input widgets) | intSlider | | label (display widgets group) | NONE | | progress bar (display widgets group) | progressBar | | vertical slider (input widgets) | intSlider | | horizontal line (input widgets) | NONE | | vertical line (input widgets) | NONE | | group box (containers group) | NONE | | tab widget (container group) | tabLayout | | main window/QWidget | window | ===== Install PythonQt binding into Operating System ===== ==== install on windows ===== * For windows, it is available as ready-to-use package install * make sure pip is up to date python -m pip install --upgrade pip * install pyside, which is same and easy python -m pip install pyside ==== install on Mac ==== * For Mac, you may find you may do quite a bit work to get it done, * you need compile ..... build... download... ok, enough * **anywhere**, you will always need to get Xcode and Command line developer tools installed * __**Xcode**__: https://developer.apple.com/xcode/download/ * __**Command Line Developer Tools**__: shell cmd: //xcode-select --install// * luckily, someone pack a similar package installer like the windows one, called PyQtX (**But unfortunately it is out of date**) * **anywhere**, you always need to download the official python (not mac built-in one) here: * __**Python Mac OS X 64-bit/32-bit installer**__ from https://www.python.org/downloads/release/python-2711/ * ok, then I tried MacPorts, it is like pip for python, it loads and installs packages for mac, - __**install macport**__: https://www.macports.org/install.php - test in command: //port// - __**install pyqt4**__: //sudo port install py27-pyqt4// - if ask for cmd line dev tools to install (actually you should have done this earlier), do this in another shell: //xcode-select --install// - after wait it run through, wow, it works, //python// then //import PyQt4// * ref: https://bitbucket.org/tortoisehg/thg/wiki/developers/MacOSX#!install-from-source-for-os-x-1011-el-capitan * Error and Fix for PyQt4 on Mac: * libqtclucene.4.dylib error, if you use everything with macports for install, then everything should work fine. * Follow exact these steps: - Install __**Xcode**__: https://developer.apple.com/xcode/download/ - Install __**Command Line Developer Tools**__: shell cmd xcode-select --install - Install __**MacPorts**__: https://www.macports.org/install.php - use MacPorts to install Python27 and py27-pyqt4 sudo port install python27 python_select sudo port select python python27 sudo port install py27-pyqt4 py27-qscintilla vi ~/.bash_profile - check following in ~/.bash_profile, then Esc > : > q PATH="/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin:${PATH}" export PATH * Error: QCocoaView handleTabletEvent warnings ====== Complex Combo to Single Code ====== **Write Python Code the way that work in any code** * the Code can break in **"Python 2 vs Python 3"**, **"PyQt* vs PySide*"**, **"Qt4 vs Qt5"** ===== Break 1: Python 2 vs Python 3 ===== * detail ref: http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html **Print Function** * python 2 print "Good Job" # works print("Good Job") # works * python 3 print("Good Job") # only * **universal code choice** print("Good Job") # upgrade old code by convert line import re old_line = 'print "Good job"' new_line = re.sub(r"(print)\s(.+)",r"\1(\2)", line) # print("Good Job") # upgrade old code by notepad++ regular replace # find : (print)\s(.+) # replace: $1\($2\), in new npp, \1(\2) **Run Python File** * python 2 execfile("testScript.py") * python 3 exec(open("testScript.py").read(), globals()) **Reload module** * python 2 reload(myModule) * python 3 import imp;imp.reload(myModule); **user input module** * python 2 a = int(raw_input()) * python 3 a = int(input()) ===== Break 2: PySide vs PyQt ===== * ref: * https://wiki.qt.io/Differences_Between_PySide_and_PyQt * http://askubuntu.com/questions/140740/should-i-use-pyqt-or-pyside-for-a-new-qt-project * **the C++ pointer to Python reference convertor library** * in PySide, is: **shiboken** * in PySide2, is: **shiboken2** * in PyQt4,PyQt5, is: **sip** * **signal connection synatx** * code in PySide, PySide2, PyQt4, BUT not in PyQt5 QtCore.QObject.connect(my_QButton, QtCore.SIGNAL("clicked()"), self.default_action) QtCore.QObject.connect(my_QAction, QtCore.SIGNAL("triggered()"), self.default_action) QtCore.QObject.connect(my_QCheckBox, QtCore.SIGNAL("toggled(bool)"), self.check_toggle_action) * code in PyQt5, **universal code for all** my_QButton.clicked.connect(self.default_action) my_QAction.triggered.connect(self.default_action) my_QCheckBox.toggled[bool].connect(self.check_toggle_action) * **File Dialog** * QtGui.QFileDialog.getOpenFileName() return value difference, note QtWidgets for Qt5 * PyQt: return filename * PySide: return (filename, extensionFilter) * old and outdate QtMiddleMan.py (just as it is for reference) will handle the proper auto detection for PyQt4 and PySide * code import sys import os default_variant = 'PySide' env_api = os.environ.get('QT_API', 'pyqt') if '--pyside' in sys.argv: variant = 'PySide' elif '--pyqt4' in sys.argv: variant = 'PyQt4' elif env_api == 'pyside': variant = 'PySide' elif env_api == 'pyqt': variant = 'PyQt4' else: variant = default_variant if variant == 'PySide': from PySide import QtGui, QtCore # This will be passed on to new versions of matplotlib os.environ['QT_API'] = 'pyside' def QtLoadUI(uifile): from PySide import QtUiTools return QtUiTools.QUiLoader().load(uifile) elif variant == 'PyQt4': import sip api2_classes = [ 'QData', 'QDateTime', 'QString', 'QTextStream', 'QTime', 'QUrl', 'QVariant', ] for cl in api2_classes: sip.setapi(cl, 2) from PyQt4 import QtGui, QtCore QtCore.Signal = QtCore.pyqtSignal QtCore.QString = str os.environ['QT_API'] = 'pyqt' def QtLoadUI(uifile): from PyQt4 import uic return uic.loadUi(uifile) else: raise ImportError("Python Variant not specified") __all__ = [QtGui, QtCore, QtLoadUI, variant] * My code on qtMode detection # ---- qtMode ---- qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5 qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5') try: from PySide import QtGui, QtCore import PySide.QtGui as QtWidgets qtMode = 0 if hostMode == "maya": import shiboken except ImportError: try: from PySide2 import QtCore, QtGui, QtWidgets qtMode = 2 if hostMode == "maya": import shiboken2 as shiboken except ImportError: try: from PyQt4 import QtGui,QtCore import PyQt4.QtGui as QtWidgets import sip qtMode = 1 except ImportError: from PyQt5 import QtGui,QtCore,QtWidgets import sip qtMode = 3 print('Qt: {0}'.format(qtModeList[qtMode])) * condition import other module based on qtMode QtNetwork = getattr(__import__(qtModeList[qtMode], fromlist=['QtNetwork']), 'QtNetwork') ===== Break 3: Qt4 vs Qt5 ===== * Qt5: * all widgets moved to "QtWidgets" class * items that are still in QtGui QIcon QKeySequence QCursor QTextFormat QPainter QPixmap QPalette ====== Qt - Common Widget Code ====== ===== Keystroke Detection ===== * modifier key detection modifiers = QtWidgets.QApplication.queryKeyboardModifiers() clickMode = 0 # basic mode if modifiers == QtCore.Qt.ControlModifier: clickMode = 1 # ctrl elif modifiers == QtCore.Qt.ShiftModifier: clickMode = 2 # shift elif modifiers == QtCore.Qt.AltModifier: clickMode = 3 # alt elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier | QtCore.Qt.AltModifier: clickMode = 4 # ctrl+shift+alt elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier: clickMode = 5 # ctrl+alt elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier: clickMode = 6 # ctrl+shift elif modifiers == QtCore.Qt.AltModifier | QtCore.Qt.ShiftModifier: clickMode = 7 # alt+shift ===== Visibility and Style ===== **Visibility control** * visibility and disable widget # disable self.uiList['namePublish_input'].setDisabled(1) self.my_pushButton.setEnabled(False) # hide self.uiList[form_ui_name].hide() # toggle hidden self.uiList['server_grp'].setVisible( self.uiList['server_grp'].isHidden() ) **Style Sheet code** * note: setSyleSheet will affect its child style as well * pushButton my_pushButton.setStyleSheet("text-align: left; color: rgb(255, 128, 128)") # set button text alignment, and text color pink * more example: http://qt-project.org/doc/qt-4.8/stylesheet-examples.html * global style set QtWidgets.QApplication.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks')) QtWidgets.QStyleFactory.keys() # Mac Only: QMacStyle as Mac # QWindowsXPStyle as WindowsXP * global style for disabled ui element or readonly element self.setStyleSheet("QLineEdit:disabled{background-color: gray;}") main_color = self.palette().color(QtWidgets.QPalette.Window).name() self.setStyleSheet('QTextEdit[readOnly="true"] { background-color: '+main_color+'; border: 0px }') * get global text color text_color = self.palette().color(QtGui.QPalette.Text).name() * list all current color palette of default in app from PySide2 import QtCore, QtGui, QtWidgets import MyToolName ui = MyToolName.main() color_info = ui.palette().color(QtGui.QPalette.Dark).name() print('color info {}'.format(color_info)) color_roles = [ QtGui.QPalette.WindowText, QtGui.QPalette.Button, QtGui.QPalette.Light, QtGui.QPalette.Midlight, QtGui.QPalette.Dark, QtGui.QPalette.Mid, QtGui.QPalette.Text, QtGui.QPalette.BrightText, QtGui.QPalette.ButtonText, QtGui.QPalette.Base, QtGui.QPalette.Window, QtGui.QPalette.Shadow, QtGui.QPalette.Highlight, QtGui.QPalette.HighlightedText, QtGui.QPalette.Link, QtGui.QPalette.LinkVisited, QtGui.QPalette.AlternateBase, QtGui.QPalette.NoRole ] print("Color roles and their corresponding colors:") for role in color_roles: color = ui.palette().color(role) print(f"{role}: {color.name()}") * apply style sheet file sshFile="darkorange.stylesheet" with open(sshFile,"r") as fh: self.setStyleSheet(fh.read()) * QListWidget style with icon-like widget elements self.uiList['proc_list'].setStyleSheet("QListWidget::item {background-color: #656565; padding: 2px;border-style: solid;border: 1px solid black;border-radius:8px; }") * app font size control self.memoData['font_size_default'] = QtGui.QFont().pointSize() self.memoData['font_size'] = self.memoData['font_size_default'] def fontNormal_action(self): self.memoData['font_size'] = self.memoData['font_size_default'] self.setStyleSheet("QLabel,QPushButton { font-size: %dpt;}" % self.memoData['font_size']) def fontUp_action(self): self.memoData['font_size'] += 2 self.setStyleSheet("QLabel,QPushButton { font-size: %dpt;}" % self.memoData['font_size']) def fontDown_action(self): if self.memoData['font_size'] >= self.memoData['font_size_default']: self.memoData['font_size'] -= 2 self.setStyleSheet("QLabel,QPushButton { font-size: %dpt;}" % self.memoData['font_size']) * get app or widget palette style sample code from PySide import QtGui groups = ['Disabled', 'Active', 'Inactive', 'Normal'] roles = ['Window', 'Background', 'WindowText', 'Foreground', 'Base', 'AlternateBase', 'ToolTipBase', 'ToolTipText', 'Text', 'Button', 'ButtonText', 'BrightText'] # ref: http://forums.cgsociety.org/showthread.php?t=1289854 def getPaletteInfo(palette = None): #build a dict with all the colors if palette == None: palette = QtGui.QApplication.palette() result = {} for role in roles: for group in groups: qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) result['%s:%s' % (role, group)] = palette.color(qGrp, qRl).rgba() return result def setPaletteFromDct(styleDict): palette = QtGui.QPalette() for role in roles: for group in groups: color = QtGui.QColor(styleDict['%s:%s' % (role, group)]) qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) palette.setColor(qGrp, qRl, color) QtGui.QApplication.setPalette(palette) # -- example # get maya app style maya_style = getPaletteInfo() # -- get a QMainWindow style piper = Piper.main() p_style = getPaletteInfo(piper.palette()) ===== Variable Access ===== * access by parent.child self.my_pushButton=my_pushButton # self is the parent, put the child as parent's property * access by Object Name root_pushButton.setObjectName("root_pushButton") my_app_instance.findChild(QtGui.QPushButton, QtCore.QString("root_pushButton")) # PyQt4 format ===== Connection ===== **common connection** * old style QtCore.QObject.connect(self.uiList['proj_choice'], QtCore.SIGNAL("activated(QString)"), self.proj_choice_action) QtCore.QObject.connect(self.uiList['item_tree'], QtCore.SIGNAL("itemExpanded(QTreeWidgetItem*)"), self.itemTreeExpand_action) QtCore.QObject.connect(self.uiList['item_tree'], QtCore.SIGNAL("itemClicked(QTreeWidgetItem*,int)"), self.itemTreeSelect_action) QtCore.QObject.connect(self.uiList['autoPublish_check'], QtCore.SIGNAL("toggled(bool)"), self.uiList['namePublish_input'].setDisabled) * new style self.uiList['proj_choice'].activated[str].connect(self.proj_choice_action) self.uiList['item_tree'].itemExpanded[QtWidgets.QTreeWidgetItem].connect(self.itemTreeExpand_action) self.uiList['item_tree'].itemClicked[QtWidgets.QTreeWidgetItem,int].connect(self.itemTreeSelect_action) self.uiList['autoPublish_check'].toggled[bool].connect(self.uiList['namePublish_input'].setDisabled) self.uiList['search_input'].textChanged.connect(self.tree_search_action) self.uiList['client_input'].returnPressed.connect(self.client_sendMsg) tree_name = 'vtx_bone_tree' self.uiList[tree_name'_search_input'].textChanged.connect( getattr(self, tree_name+"_search_action", partial(self.filter_tree,tree_name)) ) **Python Qt multiple signal to single slot connection** * method 1: partial from functools import partial self.my_tableWidget.setRowCount(len(my_List)); self.my_tableWidget.setColumnCount(2); self.my_tableWidget.setHorizontalHeaderLabels(['Current Name', 'Previous Name']); for i in range(0, len(my_List)): new_item_btn= QPushButton(self.my_tableWidget) new_item_btn.setText(my_List[i][0]) #QtCore.QObject.connect(new_item_btn, QtCore.SIGNAL("clicked()"),partial(self.buttonSelect, my_List[i][0])) # old way of connect new_item_btn.clicked.connect( partial(self.buttonSelect, my_List[i][0]) ) # new universal way of connect self.my_tableWidget.setCellWidget(i, 0, new_item_btn) def buttonSelect(self, btn_name): print btn_name * method 2: get sender() #QtCore.QObject.connect(new_item_btn, QtCore.SIGNAL("clicked()"),self.buttonSelect) # old way of connect new_item_btn.clicked.connect(self.buttonSelect) # new universal way of connect def buttonSelect(self): sender=self.sender() print str(sender.text()) **Pass User Defined Function to Class Function variable holder** * code wip ===== Hotkey and Shortcut ===== * QShortcut needs a parent widget to be able to catch user input * ref: * enum Qt::Key: http://doc.qt.io/qt-4.8/qt.html * QtGui.QKeySequence: http://doc.qt.io/qt-4.8/qkeysequence.html * wheel and modify key: http://programtalk.com/python-examples/PyQt4.QtGui.QApplication.queryKeyboardModifiers/ * QAction based shortcut example self.actionZoomIn = QtGui.QAction('Zoom In', self) self.actionZoomOut = QtGui.QAction('Zoom Out', self) key = QtCore.Qt.CTRL | QtCore.Qt.Key_Equal self.actionZoomIn.setShortcut(QtGui.QKeySequence(key)) self.actionZoomOut.setShortcut(QtGui.QKeySequence('Ctrl+-')) * Main window standalone hotkey example self.hotkey['tab_1'] = QtWidgets.QShortcut(QtGui.QKeySequence( "Ctrl+1"), self) self.hotkey['tab_1'].activated.connect( partial(self.showTab, 0) ) def showTab(self, index): self.uiList['main_tab'].setCurrentIndex(index) * per widget level hotkey example (note: defaultly focused widget shortcuts override window shortcuts ) \\ ref: https://stackoverflow.com/questions/44914888/qt-override-widget-shortcut-window-shortcut \\ #self.hotkey['node_tree_search_clear'] = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+D"), self.uiList['node_tree'], None, None, QtCore.Qt.WidgetShortcut) self.hotkey['node_tree_search_clear'] = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+D"), self.uiList['main_tab'].widget(1), None, None, QtCore.Qt.WidgetWithChildrenShortcut) self.hotkey['node_tree_search_clear'].activated.connect(self.node_tree_search_clear_action) * **Special Note**: Ctrl and Enter key is actually refer as "Ctrl+Return" ==== system wide hotkey ==== * **PyGlobalShortcut**: a simple easy module to achieve cross-platform for PyQt4, PyQt5 situation (no PySide yet) - install from source require compile, while installing from wheel can work out of box, download - src download: https://pypi.python.org/pypi/PyGlobalShortcut/ - wheel binary download: https://github.com/Asvel/pygs/releases - example import os, sys from PyQt4.QtGui import QApplication, QKeySequence from pygs import QxtGlobalShortcut SHORTCUT_SHOW = "Ctrl+Alt+S" # Ctrl maps to Command on Mac OS X SHORTCUT_EXIT = "Ctrl+Alt+F" # again, Ctrl maps to Command on Mac OS X def show_activated(): print("Shortcut Activated!") app = QApplication([]) shortcut_show = QxtGlobalShortcut() shortcut_show.setShortcut(QKeySequence(SHORTCUT_SHOW)) shortcut_show.activated.connect(show_activated) shortcut_exit = QxtGlobalShortcut() shortcut_exit.setShortcut(QKeySequence(SHORTCUT_EXIT)) shortcut_exit.activated.connect(app.exit) return_code = app.exec_() del shortcut_show del shortcut_exit sys.exit(return_code) * other windows OS specific method without extra 3rd module is using win32 API's RegisterHotKey, GetMessageA * ref: * http://timgolden.me.uk/python/win32_how_do_i/catch_system_wide_hotkeys.html ===== Event ===== * Event can be handled by - re-creation the implementation of the Class's event function - install a global event filter to handle all its childs' event * all Event types: http://doc.qt.io/qt-5/qevent.html ==== Everything about Drag and Drop in Qt ==== * The Drag to Drop event process - you start drag something (a text, a file or something) - you mouse move to a widget with "setAcceptDrops(1)" - the widget has user dragEnterEvent() and dropEvent() implemented or has "installEventFilter(handlerObj)" with a handler object. a event and object will be passed to the handing function - the "dragEnterEvent" is activated, and its handler will catch the event - event comes with "mimeData()", which can have "urls()" with "path()" for files and "text()" for texts - of course, better check "mimeData()" for "hasText()" and "hasUrls()" and "hasFormat()" - if handler determine it is ok to process, it will "accept()" for next drop event and ask handler function to return true - after "event.accept()", then "dropEvent()" will be going through drop related process with given event and object - additionally, event can also ^ Event functions ^^ | accept() | set accepted flag to indicate the receiver want the event | | ignore() | set accepted flag off | ^ QDropEvent functions (ref: http://doc.qt.io/qt-5/qdropevent.html) ^^ | acceptProposedAction() | Sets the drop action to be the proposed action | | setDropAction() | set what action the receive to do with data | | ... | QtCore.Qt.CopyAction | Copy the data to the target | | ... | QtCore.Qt.MoveAction | Move the data to the target | | ... | QtCore.Qt.LinkAction | Link the source to the target | | ... | QtCore.Qt.IgnoreAction | do nothing | | source() | return where the drag starts, can be a widget or zero for outside | Note, not all event type has source, so check the target object first | | mimeData() | | pos() | drop position | ^ QDragEnterEvent functions (ref:http://doc.qt.io/qt-5/qdragenterevent.html) ^^ | same as above || | setDropAction() | above Action option | | accept() | accept and allow drop event | **QMimeData** * full mime list: http://www.iana.org/assignments/media-types/media-types.xhtml ^ Tester ^ Getter ^ Setter ^ MIME Types ^ | hasText() | text() | setText() | text/plain | | hasHtml() | html() | setHtml() | text/html | | hasUrls() | urls() | setUrls() | text/uri-list | | hasImage() | imageData() | setImageData() | image/* | | hasColor() | colorData() | setColorData() | application/x-color | * in structure widget, like tree widget and list widget, it has its internal drag and drop and external drag and drop. ^ for external drag and drop (outside widget, can be from other App or other widget ^^ ^ my_widget.setAcceptDrops(1) | will accept drop from others | ^ my_widget.setDragEnabled(1) | will allow drag starts from itself to others | ^ for internal drag and drop ^^ | setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) | internally move around | | setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) | individual toggle select status | | setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) | standard explorer type selection handling | | setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection) | forced continuous selection | | setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) | single select | | setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) | no select allowed | * add event listener to UI element without re-implement the class by using eventFilter # add drag and drop event for a QLineEdit, so it accept file path to set its text self.uiList['proj_filePath_input'].installEventFilter(self) # let main windows handle its event # the main window event filter function def eventFilter(self, object, event): if event.type() == QtCore.QEvent.DragEnter: data = event.mimeData() urls = data.urls() if object is self.uiList['proj_filePath_input'] and (urls and urls[0].scheme() == 'file'): event.acceptProposedAction() return 1 elif event.type() == QtCore.QEvent.Drop: data = event.mimeData() urls = data.urls() if object is self.uiList['proj_filePath_input'] and (urls and urls[0].scheme() == 'file'): filePath = unicode(urls[0].path())[1:] print(filePath) self.uiList['proj_filePath_input'].setText(filePath) return 1 return 0 * For QTreeWidget, you can use 1) re-implement method (the startDrag function), 2) listener event method - the startDrag re-implement method class MyTree(QtWidgets.QTreeWidget): def __init__(self): QtWidgets.QTreeWidget.__init__(self) self.setDragEnabled(1) # setupUI node_list = [ 'file', 'place2dTexture', 'reverse'] [self.invisibleRootItem().addChild( QtWidgets.QTreeWidgetItem([x]) ) for x in node_list] def startDrag(self, dropActions ): # item drag activate this print('start drag') item = self.currentItem() mimeData = QtCore.QMimeData() mimeData.setText(item.text(0)) # work with Qt object, itself or other application #mimeData.setData('text/plain', unicode(item.text(0))) # not work as text like above print(item.text(0)) drag = QtGui.QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(QtCore.QPoint(12,12)) # this make it work, return the result action # if drag.start is old method, exec conflict in old code, use exec_ if drag.exec_(QtCore.Qt.CopyAction) == QtCore.Qt.CopyAction: print('Drag dropped ok.') # for demo purpose as below def dragMoveEvent(self): print('Move') currentNode = self.currentItem() if currentNode: print(currentNode.text(0)) def event(self, event): if event.type() == QtCore.QEvent.KeyPress and event.key() == QtCore.Qt.Key_Tab: print('Tab key now') return 1 return 0 - the event listener method for tree cur_tree = self.uiList['main_browse'].uiList['file_tree'] cur_tree.setDragEnabled(1) cur_tree.installEventFilter(self) def eventFilter(self, object, event): cur_tree = self.uiList['main_browse'].uiList['file_tree'] if object is cur_tree and event.type()== QtCore.QEvent.ChildRemoved: # the tree drag event name #print(event.type()) currentNode = cur_tree.currentItem() if currentNode: # for multi selection case path_text = '\n'.join([unicode(node.text(0)) for node in cur_tree.selectedItems()]) # for single selection case #cur_path = unicode(currentNode.text(0)) #print(cur_path) mimeData = QtCore.QMimeData() mimeData.setText(path_text) drag = QtGui.QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(QtCore.QPoint(12,12)) if drag.exec_(QtCore.Qt.CopyAction) == QtCore.Qt.CopyAction: print('Drag dropped ok.') return 1 return 0 ==== QDrag and QMimeData ==== * mimedata is a object that contains the dragging information, all applications support it and pass information in that object format. like Explorer, Nuke, etc. * Drag is the object handle the drag Action (the whole process of dragging), it contains information about dragging and that mimedata. ^ QDrag's method ^ | setMimeData() | store the mimedata | | setPixmap() | | setHotSpot() | | exec_() | start/init/exec the drag action | ===== QThread and Threading ===== * for now, most simple and straight forward process function is written in the UI class, which means when running the process function, the UI is not responding. * so common solution is using QThread object to wrap the process function, and create the QThread from main UI code, and use signal from QThread and slot on main UI to pass information around * ref: https://www.daniweb.com/programming/software-development/threads/423037/qt-signal-to-change-the-gui-out-side-the-main-thread ===== QNetwork ===== * import QtNetwork qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5 qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5') QtNetwork = getattr(__import__(qtModeList[qtMode], fromlist=['QtNetwork']), 'QtNetwork') * Server side self.server = QtNetwork.QTcpServer() self.server_connection_list = [] # responding callback function self.server.newConnection.connect(self.server_newConnection) # format prepare self.server_SIZEOF_UINT32 = 4 # sizeof_uint16 = 2 # try start server if not self.server.listen(QtNetwork.QHostAdress('0.0.0.0'), 20180): print('Server is unable to start: %s.' % self.server.errorString()) self.server.close() # get server port if it you not use a port number # self.server.serverPort() def server_newConnection(self): # get incoming client new_client = self.server.nextPendingConnection() self.server_connection_list.append(new_client) # for msg them later new_client.disconnected.connect(new_client.deleteLater) new_client.readyRead.connect(self.server_receiveMsg) # for optional 2 way msg new_client.error.connect(self.server_error) # for optional error feedback # prepare msg msg = 'Welcome from Server' reply = QtCore.QByteArray() stream = QtCore.QDataStream(reply, QtCore.QIODevice.WriteOnly) stream.setVersion(QtCore.QDataStream.Qt_4_2) stream.writeUInt32(0) # not sure why, some writes writeUInt16() stream.writeQString(msg) # some writes writeString() stream.device().seek(0) stream.writeUInt32(reply.size() - self.server_SIZEOF_UINT32) # msg client new_client.write(reply) # optional one time client msg # new_client.disconnectFromHost() # close server by self.server.close() * client side # python 2,3 support unicode function try: UNICODE_EXISTS = bool(type(unicode)) except NameError: # lambda s: str(s) # this works for function but not for class check unicode = str self.socket = QtNetwork.QTcpSocket() # read note self.nextBlockSize = 0 # responding callback function self.socket.readyRead.connect(self.client_readMsg) self.socket.disconnected.connect(self.client_serverOff) # optional server off respond self.socket.error[QtNetwork.QAbstractSocket.SocketError].connect(self.client_serverError) # error[x] to client_serverError() # or error to client_serverError(e) # socket stop self.socket.abort() # sock connect self.socket.connectToHost('127.0.0.1', 20180) def client_sendMsg(self, text): request = QtCore.QByteArray() stream = QtCore.QDataStream(request, QtCore.QIODevice.WriteOnly) stream.setVersion(QtCore.QDataStream.Qt_4_2) stream.writeUInt32(0) stream.writeQString(text) # or writeString() stream.device().seek(0) stream.writeUInt32(request.size() - self.server_SIZEOF_UINT32) self.socket.write(request) self.nextBlockSize = 0 # reset ??? # responding functions def client_readMsg(self): stream = QtCore.QDataStream(self.socket) stream.setVersion(QtCore.QDataStream.Qt_4_2) if self.nextBlockSize == 0: if self.socket.bytesAvailable() < self.server_SIZEOF_UINT32: return self.nextBlockSize = stream.readUInt32() if self.socket.bytesAvailable() < self.nextBlockSize: return textFromServer = unicode(stream.readQString()) # or readString() # process textFromServer as you like def client_serverError(self): print("Error: {}".format(self.socket.errorString())) ===== Main UI response ===== ===== menu item and action ===== * QAction can be a menu item or a tool icon button, when as menu item, it can have a checkbox instead of icon beside it. def quickMenuAction(self, objName, title, tip, icon, menuObj): self.uiList[objName] = QtWidgets.QAction(QtGui.QIcon(icon), title, self) self.uiList[objName].setStatusTip(tip) menuObj.addAction(self.uiList[objName]) self.quickMenuAction('toggleDragMode_atn','&Enable Drag Mode','Enable Drag Mode.','', cur_menu) self.uiList['toggleDragMode_atn'].setShortcut(QtGui.QKeySequence("Ctrl+E")) self.uiList['toggleDragMode_atn'].setCheckable(1) self.uiList['toggleDragMode_atn'].setChecked(0) * widget level right click menu assign process self.uiList['my_btn'].setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.uiList['my_menu'] = QtWidgets.QMenu() self.uiList['select_load_atn'] = QtWidgets.QAction('Load selection', self) self.uiList['select_add_atn'] = QtWidgets.QAction('Add to selection', self) self.uiList['my_menu'].addAction(self.uiList['select_load_atn']) self.uiList['my_menu'].addAction(self.uiList['select_add_atn']) for ui_name in self.uiList.keys(): prefix = ui_name.rsplit('_', 1)[0] if ui_name.endswith('_btn'): self.uiList[ui_name].clicked.connect(getattr(self, prefix+"_action", partial(self.default_action,ui_name))) elif ui_name.endswith('_atn'): self.uiList[ui_name].triggered.connect(getattr(self, prefix+"_action", partial(self.default_action,ui_name))) self.uiList['my_btn'].customContextMenuRequested.connect(self.my_menu_call) def my_menu_call(self, point): self.uiList['vtx_assist_menu'].exec_(self.uiList['my_btn'].mapToGlobal(point)) def select_load_action(self): print('process load') def select_add_action(self): print('process add') def my_action(self): print('btn clicked') * menu call custom and default fallback function for tree_name in ['vtx_bone_tree','vtx_vmp_tree','vtx_smp_tree', 'vtx_bsmp_tree']: self.uiList[tree_name].setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.uiList[tree_name].customContextMenuRequested.connect( getattr(self, tree_name+"_menu_call", partial(self.default_menu_call, tree_name)) ) def default_menu_call(self, ui_name, point): if ui_name in self.uiList.keys() and ui_name+'_menu' in self.uiList.keys(): self.uiList[ui_name+'_menu'].exec_(self.uiList[ui_name].mapToGlobal(point)) def vtx_bsmp_tree_menu_call(self, point): tree_name = 'vtx_bsmp_tree' cur_tree = self.uiList[tree_name] cur_node = cur_tree.currentItem() pattern = 'vtx_bsmp_tree_{0}_atn' for x in ['rename','delete','update','setBoneAll','applyAll','remove','setBone','apply','import','export']: self.uiList[pattern.format(x)].setEnabled(0) if cur_node: cur_type = str(cur_node.text(3)) if cur_type == 'bs': for x in ['rename','delete','update','setBoneAll','applyAll','remove']: self.uiList[pattern.format(x)].setEnabled(1) elif cur_type == 'attr': for x in ['rename','delete','setBone','apply','import','export']: self.uiList[pattern.format(x)].setEnabled(1) self.uiList[tree_name+'_menu'].exec_(self.uiList[tree_name].mapToGlobal(point)) ===== Simple Window ===== * toggle always on top window flag def toggleTopFlag_action(self): if self.uiList['toggleTopFlag_atn'].isChecked(): self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) self.show() else: self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowStaysOnTopHint) self.show() def toggleTop_action(self): self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowStaysOnTopHint) self.show() * in mac, there is like self.show() self.raise_() * convert QMainWindow instance into QWidget to be able into embed into other widget new_CustomMainWindowClass.setWindowFlags(QtCore.Qt.Widget) def setWidgetMode(self): self.setWindowFlags(QtCore.Qt.Widget) self.menuBar().hide() self.statusBar().hide() self.uiList['main_layout'].setContentsMargins(0, 0, 0, 0) def setWidgetMode(self, win_ui): win_ui.setWindowFlags(QtCore.Qt.Widget) win_ui.menuBar().hide() win_ui.statusBar().hide() win_ui.uiList['main_layout'].setContentsMargins(0, 0, 0, 0) * close event def closeEvent(self, event): print('Closing now') * window right click menu def contextMenuEvent(self, event): menu = QtWidgets.QMenu(self) quitAction = menu.addAction("Quit") action = menu.exec_(self.mapToGlobal(event.pos())) if action == quitAction: self.close() * window drag and move self.drag_position=QtGui.QCursor.pos() # need init it first after creation def mouseMoveEvent(self, event): if (event.buttons() == QtCore.Qt.LeftButton): self.move(event.globalPos().x() - self.drag_position.x(), event.globalPos().y() - self.drag_position.y()) event.accept() def mousePressEvent(self, event): if (event.button() == QtCore.Qt.LeftButton): self.drag_position = event.globalPos() - self.pos() event.accept() * QDialog and QMainWindow are specifically designed to be their own floating window UIs **using Qt Designer ui binding for Python** * ref: (ref: http://zetcode.com/tutorials/pyqt4/) * code import PyQt4 from PyQt4 import QtGui,QtCore, uic ui_file = 'my_qt_design_ui_file.ui' # in Qt Designer, the pushButton object name is set as "my_pushButton" form, base = uic.loadUiType(ui_file) class TestWinUI(base, form): def __init__(self): super(base,self).__init__() self.setupUi(self) self.Establish_Connections() # handler functions def my_btn_fn(self): print('do something') # action listener binding def Establish_Connections(self): # old connection QtCore.QObject.connect(self.my_pushButton, QtCore.SIGNAL("clicked()"),self.my_btn_fn) # new connection format #self.my_pushButton.clicked.connect(self.my_btn_fn) def main(): global myWin myWin=TestWinUI() myWin.show() if __name__=="__main__": main() **Manually Code GUI** * window in PyQt4 format import PyQt4 from PyQt4 import QtGui,QtCore class TestWinUI(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) # always on top self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) #location and size self.setGeometry(300, 300, 300, 300) #title self.setWindowTitle("My Tool - v1.0") #resize self.resize(250,250) #icon self.setWindowIcon(QtGui.QIcon('icon.png')) #center self.center() #fixed size self.setFixedHeight(500) self.setMinimumHeight( 0 ) self.setMaximumHeight( 2048 ) self.setMinimumSize(128, 128) self.setMaximumSize(128, 128) # maximized self.showMaximized() # show and hide self.setVisible(True) def center(self): qr = self.frameGeometry() cp = QtGui.QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def main(): global myWin myWin=TestWinUI() myWin.show() **Convert ui file to Source file** * PyQt4 case example: * for c++ code conversion from ui uic-qt4 my.ui -o myUI.cpp * for python code conversion from ui file pyuic4 my.ui -o myUI.py * note: you can find pyuic4 in the Qt package folder, same with QtDeisgner \Python27\Lib\site-packages\PyQt4 * note: for windows, it is pyuic4.bat file **compile resource like images into a py file** * if you have .qrc file, it is the link to the local resource files, you can use utility pyrccc4 (pyqt4 case) to convert into binary data contained py file, example pyrcc4 -py3 resource.qrc -o resource_rc.py * inside qrc images/icon.png * refer to resource_rc.py by import resource_rc QtGui.QPixmap(':/images/icon.png') ===== Dialog ===== * message dialog try: from PySide import QtGui, QtCore import PySide.QtGui as QtWidgets print("PySide Try") qtMode = 0 except ImportError: try: from PySide2 import QtCore, QtGui, QtWidgets print("PySide2 Try") qtMode = 2 except ImportError: try: from PyQt4 import QtGui,QtCore import PyQt4.QtGui as QtWidgets import sip qtMode = 1 print("PyQt4 Try") except ImportError: from PyQt5 import QtGui,QtCore,QtWidgets import sip qtMode = 3 print("PyQt5 Try") def quickMsg(msg): tmpMsg = QtWidgets.QMessageBox() # for simple msg that no need for translation tmpMsg.setWindowTitle("Info") tmpMsg.setText(msg) tmpMsg.addButton("OK",QtWidgets.QMessageBox.YesRole) tmpMsg.exec_() * prompt dialog def quickMsgAsk(msg, mode=0, choice=[]): # getItem, getInteger, getDouble, getText modeOpt = (QtGui.QLineEdit.Normal, QtGui.QLineEdit.NoEcho, QtGui.QLineEdit.Password, QtGui.QLineEdit.PasswordEchoOnEdit) # option: QtWidgets.QInputDialog.UseListViewForComboBoxItems if len(choice)==0: txt, ok = QtGui.QInputDialog.getText(None, "Input", msg, modeOpt[mode]) return (unicode(txt), ok) else: txt, ok = QtGui.QInputDialog.getItem(None, "Input", msg, choice, 0, 0) # current, editable return (unicode(txt), ok) * prompt dialog in list style instead of combo box def input_dialog(title, items): input_dialog = QtGui.QInputDialog() input_dialog.setComboBoxItems(items) input_dialog.setLabelText(title) #input_dialog.setComboBoxEditable(False) input_dialog.setOption(QtGui.QInputDialog.UseListViewForComboBoxItems) done = input_dialog.exec_() user_choice = input_dialog.textValue() return user_choice,done * font dialog def font_action(self): font, ok = QtWidgets.QFontDialog.getFont() if ok: self.uiList['font_label'].setFont(font) ===== Layout and Container ===== * Great ref on layout: http://doc.qt.io/qt-5/qtwidgets-layouts-basiclayouts-example.html * QVBoxLayout and QHBoxLayout my_verticalLayout.setContentsMargins(0,0,0,0) # set margin of the boundingbox of layout # UI alignment my_layout.setAlignment(QtCore.Qt.AlignTop) * QGridLayout my_grid_layout.setSpacing(0) # 0 tight grid like maya grid # -- make grid equal size for all cells cur_layout = ui['region_grid'] # for i in range( cur_layout.rowCount() ): # cur_layout.setRowStretch( i, 1) for j in range( cur_layout.columnCount() ): cur_layout.setColumnStretch( j, 1 ) * QFormLayout * QSplitter my_QSplitter = QtGui.QSplitter() my_QSplitter.setObjectName("my_QSplitter") my_QSplitter.setOrientation(QtCore.Qt.Vertical) # so the split into top to bottom panels my_QSplitter.addWidget(each_widget) # add to each split panel my_QSplitter.setSizes([200,500]) * QGroup * QTabWidget * customize look my_tab.setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }") * QScrollArea * put widget into a scroll area # main_layout is top most layout main_scroll = QtWidgets.QScrollArea() main_scroll.setWidgetResizable(1) content_widget = QtWidgets.QWidget() content_layout = QtWidgets.QVBoxLayout(content_widget) main_scroll.setWidget(content_widget) test_label = QtWidgets.QLabel('Example Text') test_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) content_layout.addWidget(tmpLabel) main_layout.addWidget(main_scroll) # optional main_layout.setStyleSheet("QScrollArea{min-width:500 px; min-height: 400px}") ===== QWidget Property ===== * set width self.uiList['filter_label'].setFixedWidth(80) * get current widget in focus and get its class name QtWidgets.QApplication.focusWidget().metaObject().className() * get parent widget my_widget.parentWidget() * get most top widget, mostly the main window object my_widget.window() * get base class for base in self.__class__.__bases__: print(base.__name__) ===== Text Widget ===== * note: in PyQt4, PyQt5, the text returned is QString instead of python str object, so you need put str(result) to get python string. For PySide, PySide2, it automatically return python str object * get label or content of widgettest.root_lineEdit.text() * python str to qt string qt_txt = QtCore.QString(u'normal text') normal_txt=str(qt_txt) * QLineEdit emit typing signal self.uiList['search_input'].textChanged.connect(self.dir_tree_search_action) * QLineEdit readOnly self.uiList['user_input'].setDisabled(1) # grey out look, can use style to tune better self.uiList['user_input'].setReadOnly(1) # normal look, but not editable * QLineEdit password hide self.uiList['user_input'].setEchoMode(QtWidgets.QLineEdit.Password) * QTextEdit make it accept plain text only # make it plain text editor as pasted self.uiList['env_txt'].setAcceptRichText(0) # get back text env_text = unicode(self.uiList['env_txt'].toPlainText()) * QTextEdit enable drag and drop files into area, (this code also works for other widget like window) def dragEnterEvent( self, event ): data = event.mimeData() urls = data.urls() if ( urls and urls[0].scheme() == 'file' ): event.acceptProposedAction() def dragMoveEvent( self, event ): data = event.mimeData() urls = data.urls() if ( urls and urls[0].scheme() == 'file' ): event.acceptProposedAction() def dropEvent( self, event ): data = event.mimeData() urls = data.urls() if ( urls and urls[0].scheme() == 'file' ): txt = "\n".join( [unicode(url.path())[1:] for url in urls] ) # remove 1st / char self.insertPlainText( txt ) * QComboBox (dropdown list, user choice) # current selected item and index self.uiList['template_choice'].currentText() self.uiList['template_choice'].currentIndex() # item count self.uiList['template_choice'].count() self.uiList['template_choice'].itemText(0) # ui response self.uiList['project_choice'].activated[str].connect(self.daily_path_update_process) self.uiList['project_choice'].activated[int].connect(self.daily_path_update_process) # make the combobox ui size auto grow to content size if possible cur_choice.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) # make the combobox dropdown size auto grow to content size, not affect combobox ui size cur_choice.view().setMinimumWidth(cur_choice.view().sizeHintForColumn(0)) # make the combobox dropdown size to a manual fix witdh cur_choice.view().setMinimumWidth(500) # make the combobox dropdown size to auto grow, but not always show to fit max content width cur_choice.view().setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) ===== Table widget ===== * **qtableview vs qtablewidget** * QTableWidget inherits QTableView * The QTableWidget class provides an item-based table view with a default model. * If you want a table that uses your own data model you should use QTableView rather than this class. * use an QTableWidget, you can insert anything into any cell with it's setCellWidget() function * QTableView is not designed to have editable headers. * A solution to achieve this can be done with a QLineEdit [doc.qt.nokia.com]. The idea is to create an edit box over the expected header section and make it behave “like” an usual cell when edited. * PyQt widgets, including QListWidget, QTableWidget, and QTreeWidget, are views with models and delegates aggregated inside them. * The widgets set on the cells have nothing to do with the contents of the table, so you won’t get signals from the table in such a case. If you have a row or column of widgets potentially emitting signals, and you want one slot to be notified of the row/column index of the widget that was triggered, then QSignalMapper * QTableWidget cell width and height cur_table.verticalHeader().setDefaultSectionSize(20) # set default row height cur_table.horizontalHeader().setDefaultSectionSize(80) # set default column width cur_table.setColumnWidth(0,200) # set colume 0 width cur_table.setRowHeight(0,20) # set row 0 height * QTable content # get table content like (cellWidget, text) for i in range(my_tableWidget.rowCount()): print( str(my_tableWidget.item(i,1).text() ) # for cell with item print( str(my_tableWidget.cellWidget(i,0).text() ) # for cell with widget * QTable add row or columne at end cur_table.insertColumn(cur_table.columnCount()) cur_table.insertRow(cur_table.rowCount()) * QTable header cur_table.setHorizontalHeaderLabels( ['name','address'] ) cur_table.setVerticalHeaderLabels( ['Admin','User'] ) * QTable add cell content cur_item = QtGui.QTableWidgetItem("Good One") cur_table.setItem(3, 0, cur_item) * QTable clear # remove everything and clear cell size as well cur_table.clear() cur_table.setRowCount(0) cur_table.setColumnCount(0) # remove content only cur_table.clearContents() ===== List Widget ===== * List widget is like Tree widget, but with only single column, so if you need anything more than 1 column, tree is the way to go **Customize look** * set alternative row color my_list.setAlternatingRowColors(1) * round corner list item my_list.setStyleSheet("QListWidget::item {background-color: #656565; padding: 2px;border-style: solid;border: 1px solid black;border-radius:8px; }") ===== Tree Widget ===== * **QTreeView vs QTreeWidget** * QTreeView is a view, just like view in database, it is interface to the database data model, and you can multiple type and design of view with same common shared data model, and you only need to update the model, and all views linking to it auto updates. * QTreeWidget is more like a widget with its own content, and you need to update the widget contently manually if you want to update its content, and its content is not shared with others, and you have to update contents for each widget even their content is the same. * Note: if you select a text element in tree widget, Ctrl+C will copy the text * QTreeView model = QtGui.QDirModel() # directory tree tree = QtGui.QTreeView(self) tree.setModel(model) * QTreeWidget # clear content self.uiList['my_tree'].clear() # header related self.uiList['my_tree'].setHeaderLabels(["Name", "URL"]) self.uiList['item_tree'].setHeaderHidden(1) # set on one column for its header label cur_tree.headerItem().setText(0,cur_version) # hide column 1 self.uiList['item_tree'].setColumnHidden(1,1) # toggle column hidden def toggle_path_visible_action(self): cur_tree = self.uiList['result_tree'] for col in [1,2]: cur_tree.setColumnHidden(col,1-cur_tree.isColumnHidden(col)) # column width (not working) self.uiList['file_tree'].setColumnWidth(0,200) self.uiList['file_tree'].setColumnWidth(1,50) # auto fix width to content for request column index self.uiList['element_tree'].resizeColumnToContents(0) # prevent above auto fit cause too small column look for empty column self.uiList['element_tree'].header().setMinimumSectionSize(100) # auto column width, and no super long last column, 2 col in this example cur_tree = self.uiList['node_tree'] cur_tree.header().setStretchLastSection(0) #if qtMode < 2: if your_qt == 'qt4': cur_tree.header().setResizeMode(0, QtWidgets.QHeaderView.Stretch) # important column stretch as much as it like cur_tree.header().setResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # not important column take what it need else: cur_tree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) # qt5 cur_tree.header().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # qt5 # sort self.uiList['file_tree'].setSortingEnabled(1) # force a sort cur_tree.sortByColumn(0, QtCore.Qt.AscendingOrder) # drag (ref: http://doc.qt.io/qt-4.8/qabstractitemview.html#DragDropMode-enum) self.uiList['config_tree'].setDragEnabled(1) self.uiList['config_tree'].setDragDropMode(QtGui.QAbstractItemView.InternalMove) # drag disable self.uiList['config_tree'].setDragEnabled(1) self.uiList['config_tree'].setDragDropMode(QtGui.QAbstractItemView.NoDragDrop) # connection and response self.uiList['item_tree'].itemExpanded[QtWidgets.QTreeWidgetItem].connect(self.itemTreeExpand_action) self.uiList['item_tree'].itemClicked[QtWidgets.QTreeWidgetItem,int].connect(self.contentUpdate_action) self.uiList['dir_tree'].itemDoubleClicked.connect( partial(self.tree_open_action,'dir_tree') ) self.uiList['dev_file_tree'].header().sectionDoubleClicked[int].connect(self.dev_file_tree_update_process) cur_tree.addTopLevelItem(new_node) cnt = cur_tree.topLevelItemCount() # QTreeWidgetItem # root root = cur_tree.invisibleRootItem() # node cur_node.addChild(item) selected_node.setExpanded(1) # add node_list = [ 'file', 'place2dTexture', 'reverse'] cur_tree.clear() [cur_tree.invisibleRootItem().addChild( QtWidgets.QTreeWidgetItem([x]) ) for x in node_list] # add : method 2 self.uiList['app_tree'].addTopLevelItems([ QtWidgets.QTreeWidgetItem([os.path.basename(txt),txt,'file']) for txt in txt_list]) # remove for item in cur_tree.selectedItems(): (item.parent() or root).removeChild(item) # make tree item editable for all columes new_node.setFlags(new_node.flags()|QtCore.Qt.ItemIsEditable) # change align new_node.setTextAlignment(i, QtCore.Qt.AlignRight ) # change bg to pink new_node.setBackgroundColor(0, QtGui.QColor(255,192,203, 100)) # set to brush and reset to default self.brush['fav'] = QtGui.QBrush(QtGui.QColor("#F280BF")) currentNode.setBackground(0, self.brush['fav']) currentNode.setData(0, QtCore.Qt.BackgroundRole, None) * make tree item editable only for some columes # ref: https://stackoverflow.com/questions/2801959/making-only-one-column-of-a-qtreewidgetitem-editable # 1. disable edit on tree level self.uiList['vtx_smp_tree'].setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.uiList['vtx_smp_tree'].itemDoubleClicked.connect( partial(self.tree_edit_action,'vtx_smp_tree') ) # 2. enable edit on tree item level new_item.setFlags(new_item.flags()|QtCore.Qt.ItemIsEditable) # 3. catch double click action on tree to enable editing box on that item in that column after checking column id def tree_edit_action(self, tree_name, item, col): cur_tree = self.uiList[tree_name] if tree_name == 'vtx_smp_tree': if col == 0: cur_tree.editItem(item, col) * expand tree # expand top node for i in range(cur_tree.topLevelItemCount()): cur_tree.topLevelItem(i).setExpanded(1) # expand all cur_tree.expandAll() # expand to depth for tree view only cur_tree.expandToDepth(1) # use this code to expand to level def tree_expand(self, node, level=0): if isinstance(node, (str, unicode)): node = self.uiList[node].invisibleRootItem() # expand top node for i in range(node.childCount()): node.child(i).setExpanded(1) # sub node if level == -1: self.tree_expand(node.child(i), level) elif level > 0: level -=1 self.tree_expand(node.child(i), level) * common tree quick creation function def tree_newNode_action(self): cur_tree = self.uiList['config_tree'] node_name,ok = self.quickMsgAsk('New Setting Name') if ok and node_name != '': self.quickTree(cur_tree, [node_name, ''], 1) # all editable def tree_removeNode_action(self): self.quickTreeRemove(self.uiList['config_tree']) def quickTree(self, cur_tree, node_data, editable=0): # not per-column control on editable if not isinstance(node_data, (list, tuple)): node_data = [node_data] # 1. get current selection selected_node = cur_tree.selectedItems() if len(selected_node) > 0: selected_node = selected_node[0] else: selected_node = cur_tree.invisibleRootItem() # 2. create a new node new_node = QtWidgets.QTreeWidgetItem() for i,name in enumerate(node_data): new_node.setText(i, name) if editable == 1: new_node.setFlags(new_node.flags()|QtCore.Qt.ItemIsEditable) # 3. add it selected_node.addChild(new_node) # 4. expand it selected_node.setExpanded(1) def quickTreeRemove(self, cur_tree): root = cur_tree.invisibleRootItem() for item in cur_tree.selectedItems(): (item.parent() or root).removeChild(item) def quickTreeInfo(self, cur_tree): data = [] root = cur_tree.invisibleRootItem() child_count = root.childCount() for i in range(child_count): cur_node = root.child(i) cur_info = [] for j in range( cur_node.columnCount() ): cur_info.append( unicode(cur_node.text(j)) ) data.append(cur_info) return data def quickTreeUpdate(self, cur_tree, data): root = cur_tree.invisibleRootItem() data_len = len(data) for i in range(data_len): self.quickTree(cur_tree, data[i], 1) * tree item selection operation self.uiList['config_tree'].itemClicked[QtWidgets.QTreeWidgetItem,int].connect(self.config_tree_select_action) def config_tree_select_action(self): currentNode = self.uiList['config_tree'].currentItem() if currentNode: self.uiList['source_txt'].setText(currentNode.text(1)) * tree data export import as list type data def TreeToData(self, tree, cur_node): col_count = tree.columnCount() child_count = cur_node.childCount() node_info = [ unicode( cur_node.text(i) ) for i in range(col_count) ] node_info_child = [] for i in range(child_count): node_info_child.append( self.TreeToData(tree, cur_node.child(i) ) ) return (node_info, node_info_child) def DataToTree(self, tree, cur_node, data): node_info = data[0] node_info_child = data[1] [cur_node.setText(i, node_info[i]) for i in range(len(node_info))] for sub_data in node_info_child: new_node = QtWidgets.QTreeWidgetItem() cur_node.addChild(new_node) self.DataToTree(tree, new_node, sub_data) * tree to node list (flat hierarchy order) def tree_to_node_list(self, cur_node): node_list = [] node_list.append(cur_node) for i in range(cur_node.childCount()): node_list.extend(self.tree_to_node_list(cur_node.child(i))) return node_list * TreeWidgetItem selectable, editable item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable) * not selectable item.setFlags(QtCore.Qt.ItemIsEnabled) * checkable with a checkbox new_item.setFlags(new_item.flags() or QtCore.Qt.ItemIsUserCheckable) new_item.setCheckState(0, QtCore.Qt.Checked) * conversion between text and checkable for plain tree data export def node_check_to_text(self,cur_node, col_list): for i in col_list: if cur_node.checkState(i): cur_node.setText(i, '1') else: cur_node.setText(i, '0') for i in range(cur_node.childCount()): self.node_check_to_text(cur_node.child(i), col_list) def node_text_to_check(self, cur_node, col_list): for i in col_list: if cur_node.text(i) == '1': cur_node.setCheckState(i, QtCore.Qt.Checked) else: cur_node.setCheckState(i, QtCore.Qt.Unchecked) cur_node.setText(i,'') for i in range(cur_node.childCount()): self.node_text_to_check(cur_node.child(i), col_list) * set active item in tree cur_tree.setCurrentItem(cur_node) # QTreewidgetItem * hide header cur_tree.header().hide() * QTreeWidgetItem set foreground(text) or background color and reset to default self.uiList['gold_brush'] = QtGui.QBrush(QtGui.QColor(255,165,0)) # gold color self.uiList['normal_brush'] = self.uiList['vtx_bone_tree'].palette().text() # get default tree foreground color # current tree widget item col=2 cur_item.setForeground(col,self.uiList['gold_brush']) # set text to gold color cur_item.setForeground(col,self.uiList['normal_brush']) # reset to normal # note: style not working for tree widget item # cur_item.setStyleSheet("color: rgb(255, 165, 0)") # not working | QtCore.Qt.NoItemFlags | 0 | It does not have any properties set | | QtCore.Qt.ItemIsSelectable | 1 | It can be selected | | QtCore.Qt.ItemIsEditable | 2 | It can be edited | | QtCore.Qt.ItemIsDragEnabled | 4 | It can be dragged | | QtCore.Qt.ItemIsDropEnabled | 8 | It can be used as a drop target | | QtCore.Qt.ItemIsUserCheckable | 16 | It can be checked or unchecked by the user | | QtCore.Qt.ItemIsEnabled | 32 | The user can interact with the item | | QtCore.Qt.ItemIsTristate | 64 | The item is checkable with three separate states | ===== Tab Widget ===== * set tab widget tab width self.uiList['main_tab'].setStyleSheet("QTabBar::tab { height: 30px; width: 60px; }") * set tab widget tab alignment self.uiList['main_tab'].setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }") * get and set active tab index self.uiList['main_tab'].currentIndex() self.uiList['main_tab'].setCurrentIndex(1) self.uiList['main_tab'].count() # return total tabs * put tab on bottom self.uiList['main_tab'].setTabPosition(QtWidgets.QTabWidget.South) * link ctrl+1,2,3,4 to the tab switch cur_tab = self.uiList['main_tab'] self.hotkey = {} for i in range( cur_tab.count() ): self.hotkey['tab_{}'.format(i)] = QtWidgets.QShortcut(QtGui.QKeySequence( "Ctrl+{}".format(i+1) ), self) self.hotkey['tab_{}'.format(i)].activated.connect(partial(cur_tab.setCurrentIndex, i)) ===== QTimer Object ===== * a continuously trigger clock setup # first, create a timer object my_timer = QtCore.QTimer() # 2nd, link alarm to a function my_timer.timeout.connect(self.time_show) # start the clock / stopwatch with a interval my_timer.start(1000) # end the clock my_timer.stop() * timer example, a proper alternative to Python sleep hostMode='' # ---- QtMode detection ---- qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5 qtModeList = ("PySide", "PyQt4", "PySide2", "PyQt5") try: from PySide import QtGui, QtCore import PySide.QtGui as QtWidgets print("PySide Try") qtMode = 0 if hostMode == "maya": import shiboken except ImportError: try: from PySide2 import QtCore, QtGui, QtWidgets print("PySide2 Try") qtMode = 2 if hostMode == "maya": import shiboken2 as shiboken except ImportError: try: from PyQt4 import QtGui,QtCore import PyQt4.QtGui as QtWidgets import sip qtMode = 1 print("PyQt4 Try") except ImportError: from PyQt5 import QtGui,QtCore,QtWidgets import sip qtMode = 3 print("PyQt5 Try") import nuke def frameHold_update(node_name, start_frame=1, end_frame=10, step_sec=3): node=nuke.toNode(node_name) node['first_frame'].setValue(start_frame) my_timer = QtCore.QTimer() # inner function for timer def update_process(): cur_val = node['first_frame'].value() # create a node called 'stop' to if you want to force it stop if cur_val < end_frame and not nuke.exists('stop'): node['first_frame'].setValue(cur_val+1) print('{0}/{1}'.format(cur_val+1, end_frame) ) else: my_timer.stop() print('finish') my_timer.timeout.connect(update_process) # start my_timer.start(1000*step_sec) # make your settting change below frameHold_update('FrameHold1', start_frame=2, end_frame=5, step_sec=2) ===== QtWebKit and QtNetwork ===== * Web and Browser related Qt coding. * universally import QtNetwork # ---- qtMode ---- qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5 qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5') try: from PySide import QtGui, QtCore import PySide.QtGui as QtWidgets qtMode = 0 if hostMode == "maya": import shiboken except ImportError: try: from PySide2 import QtCore, QtGui, QtWidgets qtMode = 2 if hostMode == "maya": import shiboken2 as shiboken except ImportError: try: from PyQt4 import QtGui,QtCore import PyQt4.QtGui as QtWidgets import sip qtMode = 1 except ImportError: from PyQt5 import QtGui,QtCore,QtWidgets import sip qtMode = 3 print('Qt: {0}'.format(qtModeList[qtMode])) QtNetwork = getattr(__import__(qtModeList[qtMode], fromlist=['QtNetwork']), 'QtNetwork') * Build a advanced browser with Qt * https://pawelmhm.github.io/python/pyqt/qt/webkit/2015/09/08/browser.html ===== Custom Widget - Screen Grab with QLabel QRubberBand ===== * a customized QLabel with QRubberBand built-in, code for Py2.7 and Py3.5 and PyQt4,5, PySide1,2 * how to use in your window class self.my_preview_screen = PreviewScreen() self.my_preview_screen.setMinimumSize(240, 160) def preview_grab_action(self): self.hide() QtCore.QTimer.singleShot(1 * 1000, self.preview_update_action) def preview_update_action(self): self.my_preview_screen.update() self.show() def preview_crop_action(): self.my_preview_screen.crop() * source code class PreviewScreen(QtWidgets.QLabel): def __init__(self): QtWidgets.QLabel.__init__(self) self.rubberBand = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self) self.screen = None self.preview = None self.origin = None def mousePressEvent(self,e): self.origin = e.pos() self.rubberBand.setGeometry(QtCore.QRect(self.origin , QtCore.QSize())); self.rubberBand.show() def mouseMoveEvent(self,e): self.rubberBand.setGeometry(QtCore.QRect(self.origin , e.pos()).normalized()); def mouseReleaseEvent(self,e): pass def update(self): self.screen = QtGui.QPixmap.grabWindow(QtWidgets.QApplication.desktop().winId()) # Qt5 grab() self.preview = self.screen.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.setPixmap(self.preview) def crop(self): if self.screen == None: return select_rect = self.rubberBand.geometry() preview_rect = self.preview.rect() cross_rect = select_rect.intersect(preview_rect) # now the widget can't be center as pixmap seems on left print(cross_rect.x(), cross_rect.y(), cross_rect.width(), cross_rect.height()) ratio = (self.screen.height()*1.0) / self.preview.height() para = [cross_rect.x(), cross_rect.y(), cross_rect.width(), cross_rect.height() ] self.screen = self.screen.copy(QtCore.QRect(*[x*ratio for x in para])) self.preview = self.screen.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.setPixmap(self.preview) self.rubberBand.hide() def save(self, filepath, filetype): if self.screen != None: self.screen.save(filepath, filetype) def load(self, filepath): self.screen = QtGui.QPixmap(filepath) self.preview = self.screen.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.setPixmap(self.preview) def clear(self): self.screen = None self.preview = None ===== mix Win32 and QWidget in each other ===== * possible solution * win32gui or ctypes and SetParent method to force hwnd based window to be in each other qwidget_hwnd = qwidget_var.winId() win32gui.SetParent(child_hwnd, parent_hwnd) ctypes.windll.user32.SetParent(child_hwnd, parent_hwnd) * ref: * https://github.com/shotgunsoftware/tk-photoshop/blob/master/python/tk_photoshop/win_32_api.py * http://code.activestate.com/lists/python-win32/11295/ * http://pyqt.riverbankcomputing.narkive.com/RNoK1vJw/convert-windows-app-to-pyqt-widget * https://stackoverflow.com/questions/31098786/opening-other-application-in-single-window-using-python * https://forums.adobe.com/thread/1343097 * Qt4 QtWinMigrate and QWinWidget: (only in Qt4) * https://area.autodesk.com/blogs/the-3ds-max-blog/pyqt-ui-in-3ds-max-2014-extension/ * https://sourceforge.net/projects/blur-dev/ * http://www.losart3d.com/?p=890 * https://stackoverflow.com/questions/293774/how-to-create-a-qwidget-with-a-hwnd-as-parent * http://www.qtcentre.org/threads/61738-Qt5-QWidget-create()-with-Win32-HWND-embedding-not-working-after-port-from-Qt4 * http://www.qtforum.org/article/18242/convert-windows-hwnd-to-qwidget-pointer.html * QWinHost * http://docs.huihoo.com/qt/solutions/4/qtwinmigrate/qwinhost.html * QThread * https://nikolak.com/pyqt-threading-tutorial/ ===== Problem and Solution ===== * Problem: **libpng warning: iCCP: known incorrect sRGB profile** * Solution: delete color profile inside the PNG file, for example, Photoshop > assign profile > don't use color management and save ====== Qt - System level code ====== ===== System Info ===== **QDesktopServices** * access desktop common location and openURL * ref: http://doc.qt.io/qt-4.8/qdesktopservices.html ** QLocale ** * get system default language local = str(QtCore.QLocale.system().name()) print(local) if local.startswith('zh') and 'CN' in self.memoData['lang']: self.setLang('CN') ** QClipboard ** * send to clipboard clipboard = QtWidgets.QApplication.clipboard() clipboard.setText('Information !!') ** QTime ** * get current time cur_time = QtCore.QTime.currentTime() cur_date = str(QtCore.QDateTime.currentDateTime().toString('d')) cur_date_format_text = str( QtCore.QDateTime.currentDateTime().toString('yyyy.MM.dd') ) text = str(cur_time.toString("hh:mm")) ===== File operation ===== * copy file and rename file QtCore.QFile.copy(src_path, new_path) QtCore.QFile.rename(src_path, new_path) ===== QtNetwork ===== * chat server example: * http://stackoverflow.com/questions/9355511/pyqt-qtcpserver-how-to-return-data-to-multiple-clients * http://stackoverflow.com/questions/8863502/chat-programming-with-pyqt-and-socket-standard-library * https://www.youtube.com/watch?v=crcPNFNQKDU * https://gist.github.com/1995eaton/97db2d6e62bb58205778 * https://www.pythonstudio.us/pyqt-programming/networking.html * https://www.reddit.com/r/Python/comments/y9up0/pyqt_qtcpsocket_problem/ * QtTCPSocket, python socket, python twisted protocol communication * QIODevice write write(const char * data, qint64 maxSize) write(const char * data) write(const QByteArray & byteArray) * http://www.chongchonggou.com/g_425289891.html * https://www.reddit.com/r/Python/comments/y9up0/pyqt_qtcpsocket_problem/ * https://github.com/pyqt/examples/blob/master/network/loopback.py * http://www.bogotobogo.com/Qt/Qt5_QTcpSocket.php * https://www.youtube.com/watch?v=1Tw15kIcQ14 * https://stackoverflow.com/questions/21233340/sending-string-via-socket-python * http://twistedmatrix.com/documents/15.0.0/core/howto/servers.html * https://pymotw.com/2/socket/binary.html * https://www.pythonsheets.com/notes/python-socket.html * https://stackoverflow.com/questions/18930835/how-can-twisted-call-qt-back * http://twistedmatrix.com/documents/15.0.0/core/howto/servers.html * http://stupidpythonideas.blogspot.com/2013/05/sockets-are-byte-streams-not-message.html * telnet, putty raw connection to the socket * https://stackoverflow.com/questions/12730293/how-does-telnet-differ-from-a-raw-tcp-connection ====== Reference and Tutorials ====== * Python 2,3 universal coding way: http://python-future.org/compatible_idioms.html#unicode-text-string-literals * PyQt4 vs PyQt5 difference list: http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html * tray icon demo: http://cognifty.com/main.page/php-qt_system_tray_icon_demo.html * **by Cognifty blog: a maker of Cognify PHP framework (Open Source) for php web app development** * Qt tutorial * https://www.youtube.com/user/VoidRealms/videos * PyQt tutorial * GUI Programming with Python: QT Edition * http://www.commandprompt.com/community/pyqt/ * http://zetcode.com/tutorials/pyqt4/ * PySide tutorial * http://qt-project.org/wiki/PySide_Video_Tutorials **Learn from others' tool** * Python, 3D, Pipe: https://fredrikaverpil.github.io ====== My Words about Qt ====== * I tried out wxWidget a little bit with Python, together called wxPython; * wxPython is quite good, as everything is defined and coded in Python, with a plain text editor, it does what you need * As I work with Maya more often, and Maya now since 2011, use QT framework as the interface, so build Maya tool in the same fashion, make me, more likely use QT in the workflow. * **old write date: 2014 01,** * after making a Python-Qt tool for Maya, I found Qt is so good for making tools, * you can make Qt interface with only code, like those mel UI commands * layout and UI control can all be done in code as well, like HTML, you can use either Dreamweaver(Qt Designer) or Notepad, the stylesheet are just like CSS code; the whole process is like HTML+Javascript+CSS * method and function call are easy in one sentence, reference of object is dot.property like. * advance widgets like table and splitter also available and easy to acccess * Qt GUI are just codes for GUI, it is convert-able from ui file to python file or c++ file * the language does not matter, the interaction are all the same * so the PyQt code are similar to C++ Qt code, easier to translate, learning either is the same * **old write date: 2016 06,** * more words for Qt, OK, so Qt is the GUI library, many language can use this library, the default combination is C++ and Qt, and another popular combination is Python and Qt. * The existing Python and Qt combination package (so called module or library) are PySide and PyQt (PyQt5, PyQt4 and older) * PySide and PyQt are quite similar, most time just change the first line "import PyQt4" into "import PySide" will get both to work; and they do have detail difference. * One tricky part is, PyQt need a commercial license to use for commercial, PySide are free to use for commercial. detail license difference here: http://askubuntu.com/questions/339723/can-i-sell-my-pyqt4-app-without-having-a-pyqt-license * that may be why, Maya comes with PySide by default, and you need compile PyQt4 yourself for Maya to use it. * **old write date: 2017 01,** * after so many years of using Python Qt, it is now, I think, the perfect solution to 3D graphic software tool solution, with PySide or PyQt, it is super fast and convinient to get the tool out in all platforms and integrate to all software support python or support python through a 3rd scripting language * so now I am moving to Qt + C++ solutions to explore more binary tools development using the same knowledge from python QT side. ====== Problem and Solution ====== * Problem: qt.qpa.fonts: Unable to open default EUDC font: "C:\\WINDOWS\\FONTS\\EUDC.TTE" * end-user-defined characters (EUDCs), just delete related regedit entry \HKEY_CURRENT_USER\EUDC\1252 * ref: https://bugreports.qt.io/browse/PYSIDE-1620