I am writing code for my collage project in pyqt5 where I need to make one help tab. I am planning to make help content-wise as most the software have as shown in the below image(the help of onlyoffice). Is there any way to write it easily?
The problem with that kind of interface, which shows multiple "tabs" embedded in the title bar, is that it's not easily doable with Qt, and you should implement the whole title bar by hand, which is not easy.
If you're looking for a simpler solution, I'd suggest to use a QTabWidget that doesn't show the tab bar if there's only one tab. If you're not already using a tabbed interface with closable tabs, you can set the tab widget to allow closable tabs and override the default methods in order to hide the close button if not really required.
class TabWidget(QtWidgets.QTabWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setDocumentMode(True)
self.setTabsClosable(True)
self.tabCloseRequested.connect(self.removeTab)
self.tabBar().hide()
def addTab(self, *args, **kwargs):
self.insertTab(-1, *args, **kwargs)
def insertTab(self, *args, **kwargs):
super().insertTab(*args)
closable = kwargs.get('closable', False)
if not closable:
index = args[0]
if index < 0:
index = self.count() - 1
for side in QtWidgets.QTabBar.LeftSide, QtWidgets.QTabBar.RightSide:
widget = self.tabBar().tabButton(index, side)
if isinstance(widget, QtWidgets.QAbstractButton):
self.tabBar().setTabButton(index, side, None)
break
self.tabBar().setVisible(self.count() > 1)
def removeTab(self, index):
super().removeTab(index)
self.tabBar().setVisible(self.count() > 1)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.tabWidget = TabWidget()
self.setCentralWidget(self.tabWidget)
self.main = QtWidgets.QWidget()
self.tabWidget.addTab(self.main, 'My program')
layout = QtWidgets.QGridLayout(self.main)
someButton = QtWidgets.QPushButton('Some button')
layout.addWidget(someButton, 0, 0)
layout.addWidget(QtWidgets.QLabel('Some label'), 0, 1)
helpButton = QtWidgets.QPushButton('Show help!')
layout.addWidget(helpButton, 0, 2)
textEdit = QtWidgets.QTextEdit()
layout.addWidget(textEdit, 1, 0, 1, 3)
self.helpTab = QtWidgets.QTextBrowser()
self.helpTab.setHtml('Hello, this is <b>help</b>!')
helpButton.clicked.connect(self.showHelp)
def showHelp(self):
for i in range(self.tabWidget.count()):
if self.tabWidget.widget(i) == self.helpTab:
break
else:
self.tabWidget.addTab(self.helpTab, 'Help!', closable=True)
self.tabWidget.setCurrentWidget(self.helpTab)
self.tabWidget.tabBar().show()
Now, since you also want some context-based help, you could hack your way through the whatsThis() feature. The "what's this" feature allows to show some context-based help in a small overlayed window when the window is in the "what's this mode" and the user clicks on a widget. We can use an event filter to detect when the user clicks on a widget and use the whatsThis() property as a context for showing the related help.
In the following example I'm using a simple dictionary that fills the QTextBrowser, but you can obviously use access to local documentation files or even the Qt Help framework.
Note that in order to use this approach, I had to install an event filter on all child widgets, and that's because Qt is able to react to "what's this" events if the widget actually has a whatsThis() property set. The trick is to set a whatsThis property for all child widgets when the window enters the what's this mode and install a specialized event filter on each of them, then uninstall the event filter as soon as the what's this mode is left.
NoWhatsThisText = '__NoWhatsThis'
NoWhatsThisValue = 'There is no help for this object'
HelpData = {
NoWhatsThisText: NoWhatsThisValue,
'someButton': 'Do something with the button',
'helpButton': 'Click the button to show this help',
'textEdit': 'Type <b>some text</b> to <i>read</i> it',
'mainWindow': 'A main window is cool!',
}
class WhatsThisWatcher(QtCore.QObject):
whatsThis = QtCore.pyqtSignal(str)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.WhatsThis:
whatsThis = source.whatsThis()
while whatsThis == NoWhatsThisText:
if not source.parent():
break
source = source.parent()
whatsThis = source.whatsThis()
self.whatsThis.emit(whatsThis)
event.accept()
return True
return super().eventFilter(source, event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
whatsThisAction = self.menuBar().addAction('What\'s this?')
whatsThisAction.triggered.connect(
QtWidgets.QWhatsThis.enterWhatsThisMode)
self.watchedWhatsThis = []
self.whatsThisWatcher = WhatsThisWatcher()
self.whatsThisWatcher.whatsThis.connect(self.showHelp)
self.installEventFilter(self.whatsThisWatcher)
someButton.setWhatsThis('someButton')
helpButton.setWhatsThis('helpButton')
textEdit.setWhatsThis('textEdit')
self.setWhatsThis('mainWindow')
def showHelp(self, context=''):
# ...
if context:
self.helpTab.setHtml(HelpData.get(context, NoWhatsThisValue))
if QtWidgets.QWhatsThis.inWhatsThisMode():
QtWidgets.QWhatsThis.leaveWhatsThisMode()
Related
Here is my code, How to show My Labels and activate respective QShortcut?. Want to show my labels(both instances) and assign respective shortcut keys to labels and activate it.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class Create_workingDict(QWidget):
def __init__(self,lblname,lblscut):
super(). __init__()
self.lblname = lblname
self.lblscut = lblscut
lbl_1 = QLabel()
lbl_1.setText(self.lblname)
lbl_2 = QLabel()
lbl_2.setText(self.lblscut)
vbox = QVBoxLayout()
vbox.addWidget(lbl_1)
vbox.addWidget(lbl_2)
self.setLayout(vbox)
self.msgSc = QShortcut(QKeySequence(f'{self.lblscut}'), self)
self.msgSc.activated.connect(lambda: QMessageBox.information(self,'Message', 'Ctrl + M initiated'))
class Mainclass(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Sample Window")
x = Create_workingDict("Accounts","Alt+A")
y = Create_workingDict("Inventory", "Ctrl+B")
def main():
app = QApplication(sys.argv)
ex = Mainclass()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Shortcuts work based on the context (and visibility) of the parent widget: as long as the parent is visible and the context is compatible, they will be triggered, otherwise they will not even be considered.
Be aware that the visibility is of the parent widgets (note the plural) is mandatory, no matter of the context: Qt has no API for a "global" shortcut, not only external to the application, but also within it: there is no shortcut that can automatically work anywhere in the app if (any of) its parent(s) is hidden. The context only ensures that the shortcut can only be activated if the active widget is part of the application (ApplicationShortcut), if the current active window is an ancestor of the shortcut parent (WindowShortcut), if any of the grand[...]parent widgets has focus (WidgetWithChildrenShortcut) or the current parent has it (WidgetShortcut).
Long story short: if the shortcut's parent is not visible (at any level), it will not be triggered.
Not only. In your code, both x and y are potentially garbage collected (they are not due to the fact that the lambda scope avoids destruction, but that's just "sheer dumb luck"), so that code would be actually prone to fail anyway if the activated signal would have been connected to an instance method.
If you want them to be available to the visible window, you must add their parent widgets to that window, even if they're not shown. Otherwise, just add the shortcuts to that window.
For instance:
class Mainclass(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Sample Window")
x = Create_workingDict("Accounts","Alt+A")
y = Create_workingDict("Inventory", "Ctrl+B")
layout = QVBoxLayout(self)
layout.addWidget(x)
layout.addWidget(y)
The only and automatic way to access a global application shortcut from any window of the application is to create a QKeySequence that is checked within an event filter installed on the application.
This is a possible, but crude implementation, so, take it as it is and consider its consequences:
class ShortCutFilter(QObject):
triggered = pyqtSignal(QKeySequence)
def __init__(self, shortcuts=None):
super().__init__()
self.shortcuts = {}
def addShortcut(self, shortcut, slot=None):
if isinstance(shortcut, str):
shortcut = QKeySequence(shortcut)
slots = self.shortcuts.get(shortcut)
if not slots:
self.shortcuts[shortcut] = slots = []
if slot is not None:
slots.append(slot)
return shortcut
def eventFilter(self, obj, event):
if event.type() == event.KeyPress:
keyCode = event.key()
mods = event.modifiers()
if mods & Qt.ShiftModifier:
keyCode += Qt.SHIFT
if mods & Qt.MetaModifier:
keyCode += Qt.META
if mods & Qt.ControlModifier:
keyCode += Qt.CTRL
if mods & Qt.ALT:
keyCode += Qt.ALT
for sc, slots in self.shortcuts.items():
if sc == QKeySequence(keyCode):
self.triggered.emit(sc)
for slot in slots:
try:
slot()
except Exception as e:
print(type(e), e)
return True
return super().eventFilter(obj, event)
def main():
app = QApplication(sys.argv)
shortcutFilter = ShortCutFilter()
app.installEventFilter(shortcutFilter)
shortcutFilter.addShortcut('alt+b', lambda:
QMessageBox.information(None, 'Hello', 'How are you'))
shortcutFilter.triggered.connect(lambda sc:
print('triggered', sc.toString())
ex = Mainclass()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
This, obviously, means that any key press event will pass through the known python bottleneck. A better solution would be to create global QActions and addAction() to any possible top level window that could accept it.
While this approach might seem more complex, it has its benefits; for instance, you have more control on the context of the shortcut: in the case above, you could trigger Alt+B from any window, including the one shown after previously triggering it, which is clearly not a good thing.
Add the layout in main widget.
Then pass the layout to the function where you are creating labels and add them to layout.
Below is the modified code.
Find the details as comments in below code.
Your main window class
class Mainclass(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Sample Window")
#CREATE THE LAYOUT HERE IN MAIN WINDOW
vbox = QVBoxLayout()
x = Create_workingDict("Accounts","Alt+A",vbox) #PASS THE LAYOUT OBJECT (vbox) AS AN ARGUMENT
y = Create_workingDict("Inventory", "Ctrl+B",vbox) #PASS THE LAYOUT OBJECT (vbox) AS AN ARGUMENT
#SET THE LAYOUT TO MAIN WINDOW
self.setLayout(vbox)
Your function where labels created.
Observe that the functions vbox = QVBoxLayout() and self.setLayout(vbox) are moved out of this function to main window.
class Create_workingDict(QWidget):
def __init__(self,lblname,lblscut,vbox): #RECEIVE LAYOUT (vbox) ARGUMENT
super(). __init__()
self.lblname = lblname
self.lblscut = lblscut
lbl_1 = QLabel()
lbl_1.setText(self.lblname)
lbl_2 = QLabel()
lbl_2.setText(self.lblscut)
vbox.addWidget(lbl_1)
vbox.addWidget(lbl_2)
self.msgSc = QShortcut(QKeySequence(f'{self.lblscut}'), self)
self.msgSc.activated.connect(lambda: QMessageBox.information(self,'Message', 'Ctrl + M initiated'))
In QMdiArea, we can select (point) to the activate subwindow. In my application, I want to select multiple subwindows (maybe using the "Ctrl" button) and set them as active windows (>=2 subwindows) and create a list pointer for them. I am trying to get pointers for more than one subwindow at the same time. Yes, activeSubWindow() gives only one window. But I wonder if I can use somthing like the "Ctrl" button in keyboard to select two subwindows and print the pointers to these subwindows. The idea is to get the widgets inside each subwindow (e.g TextEditor) at the same time to do afterward tasks, e.g., comparison
from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiArea, QAction, QMdiSubWindow, QTextEdit
import sys
class MDIWindow(QMainWindow):
count = 0
def __init__(self):
super().__init__()
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
file.addAction("cascade")
file.addAction("Tiled")
file.addAction("selected_subwindows")
file.triggered[QAction].connect(self.WindowTrig)
self.setWindowTitle("MDI Application")
def WindowTrig(self, p):
if p.text() == "New":
MDIWindow.count = MDIWindow.count + 1
sub = QMdiSubWindow()
sub.setWidget(QTextEdit())
sub.setWindowTitle("Sub Window" + str(MDIWindow.count))
self.mdi.addSubWindow(sub)
sub.show()
if p.text() == "cascade":
self.mdi.cascadeSubWindows()
if p.text() == "Tiled":
self.mdi.tileSubWindows()
if p.text()=="selected_subwindows":
"""I want to select multiple subwindows and and set as activate
windows with the "Ctrl" button and return a points fot all active windows"""
print("active windows: ", self.mdi.activeSubWindow())
app = QApplication(sys.argv)
mdi =MDIWindow()
mdi.show()
app.exec_()
Just like with normal window handling, it's not possible to have multiple active sub windows even in an MDI area.
In order to achieve a "multiple selection" system, you need to track the activation state of the sub windows, which can be tricky.
Subwindows can be activated in different ways:
by clicking on their title bar (including any of its buttons);
by clicking on its contained widget;
by programmatically activating it with setActiveSubWindow() (which is similar to selecting a normal window from the task bar);
While Qt provides the aboutToActivate signal, it's not always reliable: it is always emitted even when the top level window gets focus, so there's no direct way to know the reason of the activation.
The same also goes for the windowStateChanged signal (which is emitted after the state has changed).
For your situation, the best approach is mainly based on the mousePressEvent implementation of the subwindow, but also considering the window state changes, because you need to keep track of the current active windows whenever the activation is changed in any other way (by clicking on the widget or by using setActiveSubWindow().
Since mouse events are handled after the window activation is changed, the proper solution is to create a signal for which the emission will be delayed (scheduled), in order to know if the activation was actually achieved by a mouse button press on the subwindow (not on the child widget) and finally check if the Ctrl key was pressed in the meantime.
Please note that the following code is very basic, and you might need to do some adjustments. For instance, it doesn't consider activations for minimized windows (unlike normal windows, a subwindow could be active even if it's minimized), nor considers activations when clicking on any of the window buttons.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class SubWindow(QMdiSubWindow):
activated = pyqtSignal(object, bool)
ctrlPressed = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.windowStateChanged.connect(self.delayActivated)
self.activatedTimer = QTimer(
singleShot=True, interval=1, timeout=self.emitActivated)
def delayActivated(self, oldState, newState):
# Activation could also be triggered for a previously inactive top
# level window, but the Ctrl key might still be handled by the child
# widget, so we should always assume that the key was not pressed; if
# the activation is done through a mouse press event on the subwindow
# then the variable will be properly set there.
# Also, if the window becomes inactive due to programmatic calls but
# *after* a mouse press event, the variable has to be reset anyway.
self.ctrlPressed = False
if newState & Qt.WindowActive:
self.activatedTimer.start()
elif not newState and self.activatedTimer.isActive():
self.activatedTimer.stop()
def emitActivated(self):
self.activated.emit(self, self.ctrlPressed)
self.ctrlPressed = False
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.ctrlPressed = event.modifiers() & Qt.ControlModifier
self.activatedTimer.start()
super().mousePressEvent(event)
class MDIWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MDI Application")
self.activeWindows = []
activeContainer = QWidget()
activeLayout = QVBoxLayout(activeContainer)
activeLayout.setContentsMargins(0, 0, 0, 0)
self.activeList = QListWidget()
# Note: the following "monkey patch" is only for educational purposes
# and done in order to keep the code short, you should *not* normally
# do this unless you really know what you're doing.
self.activeList.sizeHint = lambda: QSize(150, 256)
activeLayout.addWidget(self.activeList)
self.compareBtn = QPushButton('Compare', enabled=False)
activeLayout.addWidget(self.compareBtn)
self.activeDock = QDockWidget('Selected windows')
self.activeDock.setWidget(activeContainer)
self.addDockWidget(Qt.LeftDockWidgetArea, self.activeDock)
self.activeDock.setFeatures(self.activeDock.NoDockWidgetFeatures)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
fileMenu = bar.addMenu("File")
self.newAction = fileMenu.addAction("New")
self.cascadeAction = fileMenu.addAction("Cascade")
self.tileAction = fileMenu.addAction("Tiled")
self.compareAction = fileMenu.addAction("Compare subwindows")
fileMenu.triggered.connect(self.menuTrigger)
self.compareBtn.clicked.connect(self.compare)
def menuTrigger(self, action):
if action == self.newAction:
windowList = self.mdi.subWindowList()
if windowList:
count = windowList[-1].index + 1
else:
count = 1
sub = SubWindow()
sub.index = count
sub.setWidget(QTextEdit())
sub.setWindowTitle("Sub Window " + str(count))
self.mdi.addSubWindow(sub)
sub.show()
sub.activated.connect(self.windowActivated)
elif action == self.cascadeAction:
self.mdi.cascadeSubWindows()
elif action == self.tileAction:
self.mdi.tileSubWindows()
elif action == self.compareAction:
self.compare()
def windowActivated(self, win, ctrlPressed):
if not ctrlPressed:
self.activeWindows.clear()
if win in self.activeWindows:
self.activeWindows.remove(win)
self.activeWindows.append(win)
self.activeList.clear()
self.activeList.addItems([w.windowTitle() for w in self.activeWindows])
valid = len(self.activeWindows) >= 2
self.compareBtn.setEnabled(valid)
self.compareAction.setEnabled(valid)
def compare(self):
editors = [w.widget() for w in self.activeWindows]
if len(editors) < 2:
return
it = iter(editors)
oldEditor = next(it)
while True:
try:
editor = next(it)
except:
msg = 'Documents are equal!'
break
if oldEditor.toPlainText() != editor.toPlainText():
msg = 'Documents do not match!'
break
oldEditor = editor
QMessageBox.information(self, 'Comparison result', msg, QMessageBox.Ok)
app = QApplication(sys.argv)
mdi = MDIWindow()
mdi.show()
app.exec_()
Note that I had to make some further changes to your code:
action checking should never be done by string comparison: the style or localization could potentially add mnemonics or text variations to action texts, and you'll never get your action triggered: create proper instance attributes and verify the action by object comparison instead.
the count must be an instance attribute, not a class one: if, for any reason, you have to create multiple instances of the main window, you'll get an inconsistent count; you should also consider the currently existing windows;
you should not specify signal overloads if there are no overloads at all (which is the case of QMenu.triggered) nor create local variables if they are being used only once (and their names are not that long, like self.menuBar());
Simple solution may be:
self.placeholder_text = "......."
...
...
#trigger the following code when enabled state of self.widget_name is changed
if(self.widget_name.isEnabled()):
self.widget_name.setPlaceholderText(self.placeholder_text)
else:
self.widget_name.setPlaceholderText("")
But i have a lot of QLineEdit widgets, so i searching a solution to grap all the cases.
A simple solution could be to cycle through all QLineEdit children:
for lineEdit in self.findChildren(QtWidgets.QLineEdit):
if lineEdit.isEnabled():
lineEdit.setPlaceholderText(self.placeholder_text)
else:
lineEdit.setPlaceholderText("")
But that's probably not a good approach, as you have to constantly check for all widgets, and there might be some line edits for which you don't want this behavior.
A better solution could be to subclass the line edit and override its changeEvent():
class MyLineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._placeholderText = self.placeholderText()
def setPlaceholderText(self, text):
self._placeholderText = text
if self.isEnabled():
super().setPlaceholderText(text)
def changeEvent(self, event):
if event.type() == QtCore.QEvent.EnabledChange:
super().setPlaceholderText(
self._placeholderText if self.isEnabled() else '')
return super().changeEvent(event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(w)
checkBox = QtWidgets.QCheckBox('Toggle enabled')
layout.addWidget(checkBox)
checkBox.setChecked(True)
lineEdit = MyLineEdit(placeholderText='placeholder')
layout.addWidget(lineEdit)
checkBox.toggled.connect(lineEdit.setEnabled)
w.show()
sys.exit(app.exec_())
The above code works both for line edits created with the placeholderText added to the constructor (like in the example) and with promoted widgets used in Designer.
Alternatively, you can add an event filter to all line edits for which you want to enable this feature.
self.widget_name.installEventFilter(self)
self.some_other_widget.installEventFilter(self)
# ...
def eventFilter(self, source, event):
if (isinstance(source, QtWidgets.QLineEdit) and
event.type() == QtCore.QEvent.EnabledChange):
source.setPlaceholderText(
self.placeholder_text if source.isEnabled() else '')
return super().eventFilter(source, event)
I have a QTreeView displaying data from a QStandardItemModel. One of the columns of the tree is displayed with a delegate that lets the user edit and display rich text. Below is a SSCCE that limits the editing to bold (with keyboard shortcut).
When the user is editing one of the items, how can I set it up so that in addition to toggling boldness with keyboard shortcut (CTRL-B), the user can also toggle it using the toolbar icon?
Thus far, the keyboard shortcut works great (you can double click, edit text, and CTRL-B will toggle bold). However, I haven't figured out how to connect the bold button in the toolbar to the appropriate slot:
self.boldTextAction.triggered.connect(self.emboldenText)
where I have this just sitting there doing nothing:
def emboldenText(self):
print "Make selected text bold...How do I do this?"
Things would be easy if the main window's central widget was the text editor: I could directly invoke the text editor's toggle bold method. Unfortunately, the text editor is only generated transiently by the tree view's delegate when the user double-clicks to start editing the tree.
That is, we have this complicated relationship:
QMainWindow -> QTreeView -> Delegate.CreateEditor ->
QTextEdit.toggleBold()
How do I access toggleBold() from within the main window for use by the toolbar action, especially given that the editor is only created temporarily when opened by the user?
I realize this may not be a PySide/Qt question as much as a Python/OOP question, so I've included additional potentially relevant tags. Any help with improving my word choice/jargon would be appreciated too.
SSCCE
#!/usr/bin/env python
import platform
import sys
from PySide import QtGui, QtCore
class MainTree(QtGui.QMainWindow):
def __init__(self, tree, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setCentralWidget(tree)
self.createStatusBar()
self.createBoldAction()
self.createToolbar()
self.tree = tree
#self.htmlDelegate = self.tree.itemDelegateForColumn(1)
def createStatusBar(self):
self.status = self.statusBar()
self.status.setSizeGripEnabled(False)
self.status.showMessage("In editor, keyboard to toggle bold")
def createToolbar(self):
self.textToolbar = self.addToolBar("Text actions")
self.textToolbar.addAction(self.boldTextAction)
def createBoldAction(self):
self.boldTextAction = QtGui.QAction("Bold", self)
self.boldTextAction.setIcon(QtGui.QIcon("boldText.png"))
self.boldTextAction.triggered.connect(self.emboldenText)
self.boldTextAction.setStatusTip("Make selected text bold")
def emboldenText(self):
print "Make selected text bold...How do I do this? It's stuck in RichTextLineEdit"
class HtmlTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Task', 'Priority'])
rootItem = model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('Low')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Low')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('<b>High</b>')]
item01 = [QtGui.QStandardItem('Call temp agency'), QtGui.QStandardItem('<b>Extremely</b> <i>high</i>')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
rootItem.appendRow(item1)
item1[0].appendRow(item01)
self.setModel(model)
self.expandAll()
self.resizeColumnToContents(0)
self.setToolTip("Use keyboard to toggle bold")
self.setItemDelegate(HtmlPainter(self))
class HtmlPainter(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
if index.column() == 1:
text = index.model().data(index) #default role is display (for edit consider fixing Valign prob)
palette = QtGui.QApplication.palette()
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
#Set text (color depends on whether selected)
if option.state & QtGui.QStyle.State_Selected:
displayString = "<font color={0}>{1}</font>".format(palette.highlightedText().color().name(), text)
document.setHtml(displayString)
else:
document.setHtml(text)
#Set background color
bgColor = palette.highlight().color() if (option.state & QtGui.QStyle.State_Selected)\
else palette.base().color()
painter.save()
painter.fillRect(option.rect, bgColor)
document.setTextWidth(option.rect.width())
offset_y = (option.rect.height() - document.size().height())/2
painter.translate(option.rect.x(), option.rect.y() + offset_y)
document.drawContents(painter)
painter.restore()
else:
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
def sizeHint(self, option, index):
fm = option.fontMetrics
if index.column() == 1:
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setHtml(text)
return QtCore.QSize(document.idealWidth() + 5, fm.height())
return QtGui.QStyledItemDelegate.sizeHint(self, option, index)
def createEditor(self, parent, option, index):
if index.column() == 1:
editor = RichTextLineEdit(parent)
editor.returnPressed.connect(self.commitAndCloseEditor)
return editor
else:
return QtGui.QStyledItemDelegate.createEditor(self, parent, option,
index)
def commitAndCloseEditor(self):
editor = self.sender()
if isinstance(editor, (QtGui.QTextEdit, QtGui.QLineEdit)):
self.commitData.emit(editor)
self.closeEditor.emit(editor, QtGui.QAbstractItemDelegate.NoHint)
class RichTextLineEdit(QtGui.QTextEdit):
returnPressed = QtCore.Signal()
def __init__(self, parent=None):
QtGui.QTextEdit.__init__(self, parent)
self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.setTabChangesFocus(True)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
fontMetrics = QtGui.QFontMetrics(self.font())
h = int(fontMetrics.height() * (1.4 if platform.system() == "Windows"
else 1.2))
self.setMinimumHeight(h)
self.setMaximumHeight(int(h * 1.2))
self.setToolTip("Press <b>Ctrl+b</b> to toggle bold")
def toggleBold(self):
self.setFontWeight(QtGui.QFont.Normal
if self.fontWeight() > QtGui.QFont.Normal else QtGui.QFont.Bold)
def sizeHint(self):
return QtCore.QSize(self.document().idealWidth() + 5,
self.maximumHeight())
def minimumSizeHint(self):
fm = QtGui.QFontMetrics(self.font())
return QtCore.QSize(fm.width("WWWW"), self.minimumHeight())
def keyPressEvent(self, event):
'''This just handles all keyboard shortcuts, and stops retun from returning'''
if event.modifiers() & QtCore.Qt.ControlModifier:
handled = False
if event.key() == QtCore.Qt.Key_B:
self.toggleBold()
handled = True
if handled:
event.accept()
return
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.returnPressed.emit()
event.accept()
else:
QtGui.QTextEdit.keyPressEvent(self, event)
def main():
app = QtGui.QApplication(sys.argv)
myTree = HtmlTree()
#myTree.show()
myMainTree = MainTree(myTree)
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Note for those that want the Full Tree Experience, with the button in the toolbar, here it is you can put it in the same folder as the script (change the name to boldText.png:
I think from a design point of view the top window is a sort of global. You have already described a behaviour which is treating it in that way and (as ekhumoro has said) that pretty much requires you to provide access to that top window to the editor.
One very simple way to do that is to call parent.window() in the createEditor method. Maybe something like:
parent.window().boldTextAction.triggered.connect(editor.toggleBold)
That seems to work for me.
Could someone show me how I could return a value from a wxPython Frame? When the use clicks close, I popup a message dialog asking him a question. I would like to return the return code of this message dialog to my calling function.
Thanks
Because the wxFrame has events that process via the app.MainLoop() functionality, the only way to get at the return value of a wx.Frame() is via catching an event.
The standard practice of handling events is typically from within the class which derives from wx.Window itself (e.g., Frame, Panel, etc.). Since you want code exterior to the wx.Frame to receive information that was gathered upon processing the OnClose() event, then the best way to do that is to register an event handler for your frame.
The documentation for wx.Window::PushEventHandler is probably the best resource and even the wxpython wiki has a great article on how to do this. Within the article, they register a custom handler which is an instance of "MouseDownTracker." Rather than instantiating within the PushEventHandler call, you'd want to instantiate it prior to the call so that you can retain a handle to the EventHandler derived class. That way, you can check on your derived EventHandler class-variables after the Frame has been destroyed, or even allow that derived class to do special things for you.
Here is an adaptation of that code from the wx python wiki (admittedly a little convoluted due to the requirement of handling the results of a custom event with a "calling" function):
import sys
import wx
import wx.lib.newevent
(MyCustomEvent, EVT_CUSTOM) = wx.lib.newevent.NewEvent()
class CustomEventTracker(wx.EvtHandler):
def __init__(self, log, processingCodeFunctionHandle):
wx.EvtHandler.__init__(self)
self.processingCodeFunctionHandle = processingCodeFunctionHandle
self.log = log
EVT_CUSTOM(self, self.MyCustomEventHandler)
def MyCustomEventHandler(self, evt):
self.log.write(evt.resultOfDialog + '\n')
self.processingCodeFunctionHandle(evt.resultOfDialog)
evt.Skip()
class MyPanel2(wx.Panel):
def __init__(self, parent, log):
wx.Panel.__init__(self, parent)
self.log = log
def OnResults(self, resultData):
self.log.write("Result data gathered: %s" % resultData)
class MyFrame(wx.Frame):
def __init__(self, parent, ID=-1, title="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE):
wx.Frame.__init__(self, parent, ID, title, pos, size, style)
self.panel = panel = wx.Panel(self, -1, style=wx.TAB_TRAVERSAL | wx.CLIP_CHILDREN | wx.FULL_REPAINT_ON_RESIZE)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add((25, 25))
row = wx.BoxSizer(wx.HORIZONTAL)
row.Add((25,1))
m_close = wx.Button(self.panel, wx.ID_CLOSE, "Close")
m_close.Bind(wx.EVT_BUTTON, self.OnClose)
row.Add(m_close, 0, wx.ALL, 10)
sizer.Add(row)
self.panel.SetSizer(sizer)
def OnClose(self, evt):
dlg = wx.MessageDialog(self, "Do you really want to close this frame?", "Confirm Exit", wx.OK | wx.CANCEL | wx.ICON_QUESTION)
result = dlg.ShowModal()
dlg.Destroy()
if result == wx.ID_CANCEL:
event = MyCustomEvent(resultOfDialog="User Clicked CANCEL")
self.GetEventHandler().ProcessEvent(event)
else: # result == wx.ID_OK
event = MyCustomEvent(resultOfDialog="User Clicked OK")
self.GetEventHandler().ProcessEvent(event)
self.Destroy()
app = wx.App(False)
f2 = wx.Frame(None, title="Frame 1 (for feedback)", size=(400, 350))
p2 = MyPanel2(f2, sys.stdout)
f2.Show()
eventTrackerHandle = CustomEventTracker(sys.stdout, p2.OnResults)
f1 = MyFrame(None, title="PushEventHandler Tester (deals with on close event)", size=(400, 350))
f1.PushEventHandler(eventTrackerHandle)
f1.Show()
app.MainLoop()
You can get the result of clicking the OK, CANCEL buttons from the Dialog ShowModal method.
Given dialog is an instance of one of the wxPython Dialog classes:
result = dialog.ShowModal()
if result == wx.ID_OK:
print "OK"
else:
print "Cancel"
dialog.Destroy()
A few years late for the initial question, but when looking for the answer to this question myself, I stumbled upon a built-in method of getting a return value from a modal without messing with any custom event funniness. Figured I'd post here in case anyone else needs it.
It's simply this guy right here:
wxDialog::EndModal void EndModal(int retCode)
Ends a modal dialog, passing a value to be returned from the
*wxDialog::ShowModal invocation.*
Using the above, you can return whatever you want from the Dialog.
An example usage would be subclassing a wx.Dialog, and then placing the EndModal function in the button handlers.
class ProjectSettingsDialog(wx.Dialog):
def __init__(self):
wx.Dialog.__init__(self, None, -1, "Project Settings", size=(600,400))
sizer = wx.BoxSizer(wx.VERTICAL) #main sized
sizer.AddStretchSpacer(1)
msg = wx.StaticText(self, -1, label="This is a sample message")
sizer.Add(msg, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 15)
horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
okButton = wx.Button(self, -1, 'OK')
self.Bind(wx.EVT_BUTTON, self.OnOK, okButton)
cancelBtn = wx.Button(self, -1, "Cancel")
self.Bind(wx.EVT_BUTTON, self.OnCancel, cancelBtn)
horizontal_sizer.Add(okButton, 0, wx.ALIGN_LEFT)
horizontal_sizer.AddStretchSpacer(1)
horizontal_sizer.Add(cancelBtn, 0, wx.ALIGN_RIGHT)
sizer.Add(horizontal_sizer, 0)
sizer.AddStretchSpacer(1)
self.SetSizer(sizer)
def OnOK(self, event):
self.EndModal(wx.ID_OK) #returns numeric code to caller
self.Destroy()
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL) #returns numeric code to caller
self.Destroy()
(Note: I just banged this code out quickly; didn't test the sizers)
As you can see, all you need to do is call the EndModal from a button event to return a value to whatever spawned the dialog.
I wanted to do the same thing, to have a graphical "picker" that I could run from within a console app. Here's how I did it.
# Fruit.py
import wx
class Picker (wx.App):
def __init__ (self, title, parent=None, size=(400,300)):
wx.App.__init__(self, False)
self.frame = wx.Frame(parent, title=title, size=size)
self.apple_button = wx.Button(self.frame, -1, "Apple", (0,0))
self.apple_button.Bind(wx.EVT_BUTTON, self.apple_button_click)
self.orange_button = wx.Button(self.frame, -1, "Orange", (0,100))
self.orange_button.Bind(wx.EVT_BUTTON, self.orange_button_click)
self.fruit = None
self.frame.Show(True)
def apple_button_click (self, event):
self.fruit = 'apple'
self.frame.Destroy()
def orange_button_click (self, event):
self.fruit = 'orange'
self.frame.Destroy()
def pick (self):
self.MainLoop()
return self.fruit
Then from a console app, I would run this code.
# Usage.py
import Fruit
picker = Fruit.Picker('Pick a Fruit')
fruit = picker.pick()
print 'User picked %s' % fruit
user1594322's answer works but it requires you to put all of your controls in your wx.App, instead of wx.Frame. This will make recycling the code harder.
My solution involves define a "PassBack" variable when defining your init function. (similar to "parent" variable, but it is normally used already when initiating a wx.Frame)
From my code:
class MyApp(wx.App):
def __init__ (self, parent=None, size=(500,700)):
wx.App.__init__(self, False)
self.frame = MyFrame(parent, -1, passBack=self) #Pass this app in
self.outputFromFrame = "" #The output from my frame
def getOutput(self):
self.frame.Show()
self.MainLoop()
return self.outputFromFrame
and for the frame class:
class MyFrame(wx.Frame):
def __init__(self, parent, ID, passBack, title="My Frame"):
wx.Frame.__init__(self, parent, ID, title, size=(500, 700))
self.passBack = passBack #this will be used to pass back variables/objects
and somewhere during the execution of MyFrame
self.passBack.outputFromFrame = "Hello"
so all in all, to get a string from an application
app = MyApp()
val = app.getOutput()
#Proceed to do something with val
Check this answer on comp.lang.python: Linkie
I don't think a wxFrame can return a value since it is not modal. If you don't need to use a wxFrame, then a modal dialog could work for you. If you really need a frame, I'd consider using a custom event.
It would go something like this:
(1) User clicks to close the wxFrame
(2) You override OnClose (or something like that) to pop up a dialog to ask the user a question
(3) Create and post the custom event
(4) Close the wxFrame
(5) Some other code processes your custom event
I think I just had the same problem as you. Instead of making that popup a frame, I made it a dialog instead. I made a custom dialog by inheriting a wx.dialog instead of a wx.frame. Then you can utilize the code that joaquin posted above. You check the return value of the dialog to see what was entered. This can be done by storing the value of the textctrl when the user clicks ok into a local variable. Then before it's destroyed, you get that value somehow.
The custom dialog section of this site helped me out greatly.
http://zetcode.com/wxpython/dialogs/