Python and Qt - The Story That Gets Complex

  • Qt is a GUI library, which almost any programming language can use, default designed for C++
    • Qt has currently 2 active version:
      1. Qt4: old and stable
      2. 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
      1. Python 2.7 32bit: (version I use for my own apps)
      2. Python 2.7 64bit: (version used by Maya2014 64bit, Maya2017 64bit, Houdini v15 64bit, Nuke 10 64bit)
      3. Python 3.4 32bit
      4. Python 3.4 64bit
      5. Python 3.5.1 32bit
      6. 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
      1. PySide: python + Qt4
      2. PySide2: Python + Qt5
      3. PyQt4: python + Qt4
      4. 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
  • 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
  • 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

Quick notes:

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 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
    • luckily, someone pack a similar package installer like the windows one, called PyQtX (But unfortunately it is out of date)
    • ok, then I tried MacPorts, it is like pip for python, it loads and installs packages for mac,
      1. test in command: port
      2. install pyqt4: sudo port install py27-pyqt4
        1. if ask for cmd line dev tools to install (actually you should have done this earlier), do this in another shell: xcode-select –install
      3. 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:
        1. Install Command Line Developer Tools: shell cmd
          xcode-select --install
        2. 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
        3. 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”

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);
  • 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
      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')
  • Qt5:
    • all widgets moved to “QtWidgets” class
    • items that are still in QtGui
      QIcon
      QKeySequence
      QCursor
      QTextFormat
      QPainter
      QPixmap
      QPalette

Qt - Common Widget Code

  • 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 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
  • 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()
  • 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())
  • 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

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())
  • QShortcut needs a parent widget to be able to catch user input
  • 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)

system wide hotkey

  • PyGlobalShortcut: a simple easy module to achieve cross-platform for PyQt4, PyQt5 situation (no PySide yet)
    1. install from source require compile, while installing from wheel can work out of box, download
    2. 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)
  • Event can be handled by
    1. re-creation the implementation of the Class's event function
    2. install a global event filter to handle all its childs' event

Everything about Drag and Drop in Qt

  • The Drag to Drop event process
    1. you start drag something (a text, a file or something)
    2. you mouse move to a widget with “setAcceptDrops(1)”
    3. 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
    4. the “dragEnterEvent” is activated, and its handler will catch the event
      1. event comes with “mimeData()”, which can have “urls()” with “path()” for files and “text()” for texts
      2. of course, better check “mimeData()” for “hasText()” and “hasUrls()” and “hasFormat()”
    5. if handler determine it is ok to process, it will “accept()” for next drop event and ask handler function to return true
    6. after “event.accept()”, then “dropEvent()” will be going through drop related process with given event and object
    7. 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
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

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
  • 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()))
  • 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))
  • 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)
  • 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

  • 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')
  • message dialog
    def quickMsg(msg):
        tmpMsg = QtGui.QMessageBox() # for simple msg that no need for translation
        tmpMsg.setWindowTitle("Info")
        tmpMsg.setText(msg)
        tmpMsg.addButton("OK",QtGui.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)
  • 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 
  • 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; }")
  • 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()
  • 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
  • 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)
  • 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 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; }")
  • 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)
     
    # 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
    self.uiList['file_tree'].setColumnWidth(0,200)
    self.uiList['file_tree'].setColumnWidth(1,50)
     
    # 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)
  • 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 top nodes
    # expand top node
    for i in range(cur_tree.topLevelItemCount()):
        cur_tree.topLevelItem(i).setExpanded(1)
  • 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
  • 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(QtGui.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))
  • 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)
  • 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
  • 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

QDesktopServices

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"))
  • copy file and rename file
    QtCore.QFile.copy(src_path, new_path)
    QtCore.QFile.rename(src_path, new_path)

Reference and Tutorials

Learn from others' tool

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.