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): 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
- PyQt6: python + Qt6
- PySide6: python + Qt6
- 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 |
|
Python 3.12 | 64bit | PyQt6, PySide | Qt6 |
- 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
- Qt6 library is modern UI with high-DPI displays support on all platforms, also the python binding is using snake case standard.
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/
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
- 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
- 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
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:
- 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)
- example code
FILE_FILTERS = [ "Portable Network Graphics files (*.png)", "Text files (*.txt)", "Comma Separated Values (*.csv)", "All files (*)", ] initial_filter = FILE_FILTERS[3] # Select one from the list. filters = ";;".join(FILE_FILTERS) filename, selected_filter = QFileDialog.getOpenFileName( self, filter=filters, initialFilter=initial_filter) print("Result:", filename, selected_filter)
- old and outdate QtMiddleMan.py (just as it is for reference) will handle the proper auto detection for PyQt4 and PySide
- code
- QtMiddleMan.py
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
Break 4: Qt6 for PyQt6 and PySide6
Date: 2024-10-15
After Qt6 for python is out, more function code has change to python snake case style, and you can directly get and change the attribute value of Qt widget object, instead of using old setXXX() and getXXX()
in the import part, enable new snake case of function naming
# Import snake_case and true_property after PySide6 imports. from __feature__ import snake_case, true_property
new way of coding, some old function is removed
# qt4, 5 main_window.setWindowTitle("My App") main_window.setWindowIcon(my_icon) # qt6 main_window.window_title = "My App" main_window.window_icon = QIcon(r"d:\test\test.png")
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
- 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())
-
- python version
from PySide6.QtGui import QPalette, QColor from PySide6.QtCore import Qt DARKPALETTE = QPalette() DARKPALETTE.setColor(QPalette.Window, QColor(53, 53, 53)) DARKPALETTE.setColor(QPalette.WindowText, Qt.white) DARKPALETTE.setColor(QPalette.Disabled, QPalette.WindowText, QColor(127, 127, 127)) DARKPALETTE.setColor(QPalette.Base, QColor(42, 42, 42)) DARKPALETTE.setColor(QPalette.AlternateBase, QColor(66, 66, 66)) DARKPALETTE.setColor(QPalette.ToolTipBase, Qt.white) DARKPALETTE.setColor(QPalette.ToolTipText, Qt.white) DARKPALETTE.setColor(QPalette.Text, Qt.white) DARKPALETTE.setColor(QPalette.Disabled, QPalette.Text, QColor(127, 127, 127)) DARKPALETTE.setColor(QPalette.Dark, QColor(35, 35, 35)) DARKPALETTE.setColor(QPalette.Shadow, QColor(20, 20, 20)) DARKPALETTE.setColor(QPalette.Button, QColor(53, 53, 53)) DARKPALETTE.setColor(QPalette.ButtonText, Qt.white) DARKPALETTE.setColor(QPalette.Disabled, QPalette.ButtonText, QColor(127, 127, 127)) DARKPALETTE.setColor(QPalette.BrightText, Qt.red) DARKPALETTE.setColor(QPalette.Link, QColor(42, 130, 218)) DARKPALETTE.setColor(QPalette.Highlight, QColor(42, 130, 218)) DARKPALETTE.setColor(QPalette.Disabled, QPalette.Highlight, QColor(80, 80, 80)) DARKPALETTE.setColor(QPalette.HighlightedText, Qt.white) DARKPALETTE.setColor( QPalette.Disabled, QPalette.HighlightedText, QColor(127, 127, 127), ) # app.setPalette(DARKPALETTE) # using app set palette only works fully right after app creation, not during runtime
- get default palette
def get_palette(root_widget: QWidget): # Call the method correctly palette = root_widget.palette default_palette = QPalette() # Here, replicate the palette settings similarly to above, using the method calls for role in [ QPalette.Window, QPalette.WindowText, QPalette.Base, QPalette.AlternateBase, QPalette.ToolTipBase, QPalette.ToolTipText, QPalette.Text, QPalette.Dark, QPalette.Shadow, QPalette.Button, QPalette.ButtonText, QPalette.BrightText, QPalette.Link, QPalette.Highlight, QPalette.HighlightedText, ]: default_palette.set_color(role, palette.color(role)) if role in [ QPalette.WindowText, QPalette.Text, QPalette.ButtonText, QPalette.Highlight, QPalette.HighlightedText, ]: default_palette.set_color( QPalette.Disabled, role, palette.color(QPalette.Disabled, role) ) return default_palette
- alternatively set palette using widget instead application.
self.default_style = get_palette(self.top_level_widget()) # or self def setting_style_dark_toggle_action(self): use_dark_mode = self.ui["setting_style_dark_check"].checked if use_dark_mode: self._change_palette_recursive(self, DARKPALETTE) else: self._change_palette_recursive(self, self.default_style) def _change_palette_recursive(self, root: QWidget, palette: QPalette): root.palette = palette for child in root.children(): if isinstance(child, QWidget): self._change_palette_recursive(child, 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 <python>
wip </code>
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
- test_global_hotkey.py
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:
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
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
- PyQtDemo.py
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
- CodeWinUI.py
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
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>images/icon.png</file> </qresource> </RCC>
- refer to resource_rc.py by
import resource_rc QtGui.QPixmap(':/images/icon.png')
online resource
- load url image
import requests from io import BytesIO try: # Fetch the image from the URL response = requests.get(preview_path) response.raise_for_status() # Convert the content to a QPixmap pixmap = QPixmap() pixmap.load_from_data(BytesIO(response.content).read()) # Set the pixmap to the label and scale to thumbnail size preview_label.pixmap = pixmap.scaled(256, 256, Qt.KeepAspectRatio) except requests.exceptions.HTTPError as http_err: if response.status_code == 404: preview_label.text = "No preview found" # Handle 404 error else: preview_label.text = "HTTP error occurred" except Exception as e: preview_label.text = "Failed to load image"
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 widget
test.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) # but item click event only update based on mouse click, whilete current item changed is better as even up/down key can trigger self.uiList['config_tree'].currentItemChanged.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
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:
- Qt4 QtWinMigrate and QWinWidget: (only in Qt4)
- http://www.qtcentre.org/threads/61738-Qt5-QWidget-create()-with-Win32-HWND-embedding-not-working-after-port-from-Qt4
- QWinHost
- QThread
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
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:
- QtTCPSocket, python socket, python twisted protocol communication
- QIODevice write
write(const char * data, qint64 maxSize) write(const char * data) write(const QByteArray & byteArray)
-
- telnet, putty raw connection to the socket
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
- PyQt tutorial
- GUI Programming with Python: QT Edition
- PySide tutorial
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.
Portable Quick UI creation code
My portable codes of quickly creation of UI by code
- auto_ui.py
from enum import Enum from typing import List, Union, Dict, Optional from functools import partial from PySide6.QtCore import Slot, Qt from PySide6.QtWidgets import ( QWidget, QSpacerItem, QPushButton, QCheckBox, QVBoxLayout, QBoxLayout, QGridLayout, QLayout, QSplitter, QGroupBox, QTabWidget, ) # Import snake_case and true_property after PySide6 imports. from __feature__ import snake_case, true_property class LayoutOption(Enum): HORIZONTAL = "h" VERTICAL = "v" class AutoUI: def __init__(self): self.ui = {} # add main layout reference self.main_layout = QVBoxLayout() self.ui["main_layout"] = self.main_layout def setup_ui(self): # batch ui initialize for name, obj in self.ui.items(): # set objectName same as reference name obj.objectName = name # connect button to matching name funcitons prefix = name.rsplit("_", 1)[0] if isinstance(obj, QPushButton): obj.clicked.connect( getattr( self, prefix + "_action", partial(self.default_action, name) ) ) if isinstance(obj, QCheckBox): obj.toggled[bool].connect( getattr( self, prefix + "_toggle_action", partial(self.default_action, name), ) ) @Slot() def default_action(self, ui_name, *argv): print("No action defined for this UI element: " + ui_name) def auto_layout( self, name_list: List[str], layout_name: str, layout_option: Optional[Union[LayoutOption, Dict[str, str]]] = None, ): if layout_name not in self.ui: return layout_object = self.ui[layout_name] # check support layout object if not isinstance( layout_object, ( QLayout, QSplitter, QGroupBox, QTabWidget, ), ): return filter_name_list = [name for name in name_list if name in self.ui] # special case for QGroupBox if isinstance(layout_object, QGroupBox): layout_object = layout_object.layout() # Determine the type of layout and call the corresponding handler if isinstance(layout_object, QBoxLayout): self._add_to_box_layout(layout_object, filter_name_list) elif isinstance(layout_object, QGridLayout): self._add_to_grid_layout(layout_object, filter_name_list, layout_option) # elif isinstance(layout_object, QFormLayout): # self._add_to_form_layout(layout_object, filter_name_list, layout_option) elif isinstance(layout_object, QSplitter): self._add_to_splitter(layout_object, filter_name_list) elif isinstance(layout_object, QTabWidget): self._add_to_tab_widget(layout_object, filter_name_list, layout_option) def _add_to_box_layout( self, layout_object: QBoxLayout, filter_name_list: List[str] ): for name in filter_name_list: ui_object = self.ui[name] if isinstance(ui_object, QWidget): layout_object.add_widget(ui_object) elif isinstance(ui_object, QSpacerItem): layout_object.add_item(ui_object) elif isinstance(ui_object, QLayout): layout_object.add_layout(ui_object) def _add_to_grid_layout( self, layout_object: QGridLayout, filter_name_list: List[str], layout_option: Optional[LayoutOption], ): # case: grid: one row/colume operation only row_count = layout_object.row_count() col_count = layout_object.column_count() for i, name in enumerate(filter_name_list): ui_object = self.ui[name] x = row_count if layout_option == LayoutOption.HORIZONTAL else i y = i if layout_option == LayoutOption.HORIZONTAL else col_count if isinstance(ui_object, QWidget): layout_object.add_widget(ui_object, x, y) elif isinstance(ui_object, QSpacerItem): layout_object.add_item(ui_object, x, y) elif isinstance(ui_object, QLayout): layout_object.add_layout(ui_object, x, y) def _add_to_splitter(self, layout_object: QSplitter, filter_name_list: List[str]): for name in filter_name_list: ui_object = self.ui[name] if isinstance(ui_object, QWidget): layout_object.add_widget(ui_object) elif isinstance(ui_object, QLayout): tmp_holder = QWidget() tmp_holder.set_layout(ui_object) layout_object.add_widget(tmp_holder) def _add_to_tab_widget( self, layout_object: QTabWidget, filter_name_list: List[str], layout_option: Dict[str, str], ): for i, name in enumerate(filter_name_list): tab_content = self.ui[name] tab_name = "tab_" + str(i) if name in layout_option: tab_name = layout_option[name] if isinstance(tab_content, QWidget): layout_object.add_tab(tab_content, tab_name) elif isinstance(tab_content, QLayout): tmp_holder = QWidget() tmp_holder.set_layout(tab_content) layout_object.add_tab(tmp_holder, tab_name)
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