wxWidgets TaskBarIcon as drop target - wxwidgets

How can I define the tray icon as a drop target using wxWidgets?
SetDropTarget is neither available in the TaskBarIcon nor in the Icon class.
I would like to have something like:
class TextDropTarget(wx.TextDropTarget):
def __init__(self, obj):
wx.TextDropTarget.__init__(self)
self.obj = obj
def OnDropText(self, x, y, data):
self.obj.action(data)
class TaskBarIcon(wx.adv.TaskBarIcon):
def __init__(self):
super().__init__()
self.SetIcon(wx.Icon(wx.Bitmap(TRAY_ICON)), TRAY_TOOLTIP)
self.SetDropTarget(TextDropTarget(self))
def action(self, data):
# Do something

After some research I can answer the question myself:
On Mac this is possible, because it's a common feature on this OS.
On Windows and Linux this is possible through an ugly workaround. I'm wondering why this is not supported on these OS, because there are indeed some use cases for this feature.
Here is the workaround (which is also mentioned here) for Windows:
Get the system tray area:
def FindSysPagerWindow():
hWnd = win32gui.FindWindowEx(win32gui.GetDesktopWindow(), 0, "Shell_TrayWnd", None)
if hWnd:
hWnd = win32gui.FindWindowEx(hWnd, None, "TrayNotifyWnd", None)
if hWnd:
hWnd = win32gui.FindWindowEx(hWnd, None, "SysPager", None)
return hWnd
class TaskBarIcon(wx.adv.TaskBarIcon):
def __init__(self, r):
super().__init__()
hSysPager = FindSysPagerWindow()
# Get rectangle of system area
self.region = win32gui.GetWindowRect(hSysPager)
self.frm = None
self.source = None
Create a thread which detects drag start events and creates a transparent frame above the system tray:
def callback(self, hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime):
length = user32.GetWindowTextLengthW(hwnd)
title = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, title, length + 1)
if self.frm is None and (title.value == "Drag"):
self.source = GetProcessFilename(GetProcessId(dwEventThread, hwnd))
self.frm = SystemTrayFrame(self.region, self.onDrop)
def DragDetectThread(self):
ole32.CoInitialize(0)
WinEventProc = WinEventProcType(self.callback)
user32.SetWinEventHook.restype = ctypes.wintypes.HANDLE
hookId = user32.SetWinEventHook(win32con.EVENT_OBJECT_SHOW, win32con.EVENT_OBJECT_SHOW,
0, WinEventProc, 0, 0, win32con.WINEVENT_OUTOFCONTEXT)
msg = ctypes.wintypes.MSG()
while user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) != 0:
user32.TranslateMessageW(msg)
user32.DispatchMessageW(msg)
user32.UnhookWinEvent(hookId)
ole32.CoUninitialize()
A still unresolved problem here is that the detection of the correct drag source is not working reliable. If the mouse is moved too fast at the drag start, then the detected source may be wrong. But this is only a problem if this information is important.
Create a listener for mouse button events, using pynput, to detect left mouse button up event, which is interpreted as a drag end event. The listener and also the onDrop method destroy the transparent frame:
from pynput.mouse import Listener, Button
...
self.listener = Listener(on_click=self.onMouseButtonEvent)
self.listener.start()
def onMouseButtonEvent(self, x, y, button, pressed):
if self.frm is not None and (button == Button.left) and not pressed:
self.frm.Destroy()
self.frm = None
def onDrop(self, x, y, data):
# Do something with the dropped data
if self.frm is not None:
self.frm.Destroy()
self.frm = None
The class for the transparent frame looks something like this:
class SystemTrayFrame(wx.Frame):
def __init__(self, r, cbDrop):
super().__init__(None, wx.ID_ANY, "TransparentFrame", pos=(r[0], r[1]), size=(r[2] - r[0], r[3] - r[1]),
style=wx.STAY_ON_TOP)
dropTarget = DropTarget(cbDrop)
self.SetDropTarget(dropTarget)
self.SetTransparent(0)
self.Show()
Up to here this is all fine if it is ok that the whole system tray area is the drop target for your application. But if you want to limit the drop area to your system tray icon you need now the ugly workaround:
a) Replace your fancy system tray icon with a uniquely colored icon with a shape which you can easily detect.
b) Make a screenshot of the system tray area, replace the system tray icon back to your fancy application icon, then search for the position of your uniquely colored icon:
im = ImageGrab.grab(bbox=self.region)
# Search for icon position and size (because of optional scaling by OS)
The search operation can be a bit more complicated when scaling was enabled by the OS.
c) Use this result for positioning the transparent frame.
Hope this helps other people who are running into the same problem.

Related

Implementing copy and move when dropping an Item in `QTreeView`

I revised the whole question because the behavior I want is hard to implement and actually use.
I'm trying to imitate the behavior in the File Explorer where when I press Shift while dragging, the file will be moved instead of copied.
This is the behavior I'm trying to imitate:
The behavior: is I'm using my LeftClick for selecting, and dragging.
About The behavior itself:
I overridden the mousePressEvent and mouseMoveEvent to start the drag. When the drag is created, it uses QTimer to detect if I pressed the Control and Shift modifier. Once a modifier is detected it sets the default drop action using setDefaultDropAction. (I think I should use setDropAction but It's only available in the dragMoveEvent and I'm doing it inside the QDrag Class)
The Issues:
Part of the behavior is working now but there is still some issues.
Even I press Shift, the DropIndicator is not changing from + to ->
Related to the issue above, The dropAction is only copyAction instead of moveAction even I'm pressing the Shift key.
My Question: What causes these issues? My gut tells me that I should've used setDropAction instead of setDefaultDropAction but again it's only available in the dragMoveEvent
My Testing Code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class ModifiedQDrag(QDrag):
def __init__(self, source):
super().__init__(source)
self.timer = QTimer(self)
self.timer.timeout.connect(self.process_event)
self.timer.setInterval(100)
self.timer.start()
def process_event(self):
if qApp.keyboardModifiers() & Qt.ControlModifier:
self.source().setDefaultDropAction(Qt.CopyAction)
elif qApp.keyboardModifiers() & Qt.ShiftModifier:
print("shift pressed")
self.source().setDefaultDropAction(Qt.MoveAction)
class Tree(QTreeView):
def __init__(self):
super().__init__()
self.setDragDropMode(QAbstractItemView.DragDrop)
self.setDropIndicatorShown(True)
self.viewport().setAcceptDrops(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# -- mouse dragging -- #
def mousePressEvent(self, event):
if event.button() == Qt.RightButton:
self.dragStartPosition = event.pos()
return super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() != Qt.RightButton:
return
if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()):
return
drag = ModifiedQDrag(self)
mimeData = QMimeData()
mimeData = self.model().mimeData([self.indexAt(event.pos())])
drag.setMimeData(mimeData)
dragAction = drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction)
return super().mouseMoveEvent(event)
def dragMoveEvent(self, event):
m = event.mimeData()
if m.hasUrls():
event.accept()
return
event.ignore()
def dropEvent(self, event):
print("[drop event] - dropped")
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
# -- left side -- #
left_side_dir = r"<Dir>"
self.model = QFileSystemModel()
self.model.setRootPath(left_side_dir)
self.tree = Tree()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(left_side_dir))
# -- right side -- #
right_side_dir = r"<Dir>"
self.model2 = QFileSystemModel()
self.model2.setRootPath(right_side_dir)
self.tree2 = Tree()
self.tree2.setModel(self.model2)
self.tree2.setRootIndex(self.model2.index(right_side_dir))
# -- layout -- #
self.tree_layout = QHBoxLayout()
self.tree_layout.addWidget(self.tree)
self.tree_layout.addWidget(self.tree2)
self.setLayout(self.tree_layout)
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
Qt can only react to mouse movements in order to trigger changes in the drop action: as the name suggests, dragMoveEvent() can only be called by a mouse move.
Considering that, a possible solution is to manually force the mouse movement whenever the keyboard modifiers change. In this way you don't even need to create a QDrag subclass and you can keep the default behavior.
Be aware that to properly get modifiers, you should not use keyboardModifiers(), but queryKeyboardModifiers(), as the first is only reliable when keyboard events are directly handled and might not be updated with the actual current state of the keyboard.
class Tree(QTreeView):
# ...
def checkDrag(self):
modifiers = qApp.queryKeyboardModifiers()
if self.modifiers != modifiers:
self.modifiers = modifiers
pos = QCursor.pos()
# slightly move the mouse to trigger dragMoveEvent
QCursor.setPos(pos + QPoint(1, 1))
# restore the previous position
QCursor.setPos(pos)
def mouseMoveEvent(self, event):
if event.buttons() != Qt.RightButton:
return
if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()):
return
self.modifiers = qApp.queryKeyboardModifiers()
# a local timer, it will be deleted when the function returns
dragTimer = QTimer(interval=100, timeout=self.checkDrag)
dragTimer.start()
self.startDrag(Qt.MoveAction|Qt.CopyAction)
def dragMoveEvent(self, event):
if not event.mimeData().hasUrls():
event.ignore()
return
if qApp.queryKeyboardModifiers() & Qt.ShiftModifier:
event.setDropAction(Qt.MoveAction)
else:
event.setDropAction(Qt.CopyAction)
event.accept()

Handle Event outside of init class pyqt5

Using the code from Here and There, I made a GUI presenting my project on a smaller scale.
I have a qTableView,containing a large array of rows, and on each rows I have a delete and an edit button. On click, it should either edit or delete the current row. When using only the first source, it works exactly as intended, but as soon as I handle the click outside of the buttons class, it stops working.
Everytime I try to edit or delete, the button that either self.sender() or QtWidgets.qApp.focusWidget() sees as the sender has the coordinates [0,0], even if it's absolutely not it's coordinates.
I have searched on various websites and can't find this precise question.
What am I doing wrong, and what could I do to solve this problem?
My code :
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QMessageBox
class EditButtonsWidget(QtWidgets.QWidget):
# Credit to : https://stackoverflow.com/a/29764914/13812144
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtWidgets.QHBoxLayout()
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.editButton = QtWidgets.QPushButton('edit')
self.deleteButton = QtWidgets.QPushButton('del')
self.buttonRow = 0
# add your buttons
layout.addWidget(self.editButton)
layout.addWidget(self.deleteButton)
self.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self,parent)
self.table = QtWidgets.QTableWidget()
self.table.setColumnCount(3)
self.setCentralWidget(self.table)
data1 = ['row1','row2','row3','row4']
data2 = ['1','2.0','3.00000001','3.9999999']
self.table.setRowCount(4)
for index in range(4):
item1 = QtWidgets.QTableWidgetItem(data1[index])
self.table.setItem(index,0,item1)
item2 = QtWidgets.QTableWidgetItem(data2[index])
self.table.setItem(index,1,item2)
self.btn_sell = EditButtonsWidget()
self.btn_sell.editButton.clicked.connect(self.handleButtonClicked)
self.table.setCellWidget(index,2,self.btn_sell)
def handleButtonClicked(self):
#button = QtWidgets.qApp.focusWidget()
button = self.sender()
index = self.table.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = MainWindow()
MainWindow.show()
sys.exit(app.exec_())
The position must be of the widget that is set in the QTableWidget, not of one of its children.
In this case it is better to consider the EditButtonsWidget as a black box and expose the clicked signals of the buttons as new signals so that the sender is EditButtonsWidget and no longer the buttons:
class EditButtonsWidget(QtWidgets.QWidget):
edit_clicked = QtCore.pyqtSignal()
delete_clicked = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtWidgets.QHBoxLayout(self)
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.editButton = QtWidgets.QPushButton('edit')
self.deleteButton = QtWidgets.QPushButton('del')
# add your buttons
layout.addWidget(self.editButton)
layout.addWidget(self.deleteButton)
self.editButton.clicked.connect(self.edit_clicked)
self.deleteButton.clicked.connect(self.delete_clicked)
for index in range(4):
item1 = QtWidgets.QTableWidgetItem(data1[index])
self.table.setItem(index,0,item1)
item2 = QtWidgets.QTableWidgetItem(data2[index])
self.table.setItem(index,1,item2)
self.btn_sell = EditButtonsWidget()
self.btn_sell.edit_clicked.connect(self.handleButtonClicked) # <---
self.table.setCellWidget(index,2,self.btn_sell)
Widget positions always use the parent's coordinate system as a reference.
In your case, the button is a child of EditButtonsWidget, and since it's also the first widget and the layout has no margins, the button is placed at 0, 0 in that coordinate reference system.
A theoretical solution to your problem would be to map the widget position to the actual widget you need a reference for, which is the viewport of the scroll area (the table):
def handleButtonClicked(self):
button = self.sender()
viewportPosition = button.mapTo(self.table.viewport(), QtCore.QPoint())
index = self.table.indexAt(viewportPosition)
if index.isValid():
print(index.row(), index.column())
The mapping is done using an empty QPoint, since the top-left corner of a widget is always 0, 0 in local coordinates.
While this works, it's not the most logic nor elegant or safest way to do so, as you should reference the actual index instaed.
A better solution would be to map the table index, use that as argument of the widget constructor, and send that index for a custom signal.
class EditButtonsWidget(QtWidgets.QWidget):
editClicked = QtCore.pyqtSignal(object)
def __init__(self, index):
super(EditButtonsWidget,self).__init__()
self.index = index
# ...
self.editButton.clicked.connect(lambda: self.editClicked.emit(index))
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
# ...
for index in range(4):
# ...
persistenIndex = QtCore.QPersistentModelIndex(
self.table.indexFromItem(item2))
self.btn_sell = EditButtonsWidget(persistenIndex)
self.btn_sell.editClicked.connect(self.handleButtonClicked)
self.table.setCellWidget(index,2,self.btn_sell)
def handleButtonClicked(self, index):
if index.isValid():
print(index.row(), index.column())
Note that I used a QPersistentModelIndex, which ensures that the model index coordinates are always consistent even if the model changes (by deleting/inserting items or moving them).
Also note that you cannot directly use a QPersistentModelIndex for most functions that take a normal QModelIndex as parameter; in case you need that, you can recreate a QModelIndex like this:
modelIndex = self.table.model().index(
persistentIndex.row(), persistentIndex.column())

PyQt5 QTableView selected cell background with Delegate

I have an application which uses a QTableView/QAbstractTableModel combination. For the view, I've defined a Delegate which displays an image (a QPixmap, loaded from an image file) in one column of the table view.
Basically, the problem is that when a cell in the column with the Delegate is selected, sometimes the background shows and sometimes it doesn't.
Here is what I've discovered by experimentation so far, and I can't make much sense of it:
I have this relatively short test program:
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = [[]], headers = None, parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__data = data
def rowCount(self, parent):
return len(self.__data)
def columnCount(self, parent):
return len(self.__data[0])
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
value = self.__data[row][column]
return value
def flags(self, index):
return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsSelectable
# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
if (index.column() == 0):
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
tableView = QtWidgets.QTableView()
tableView.setItemDelegateForColumn(0, Delegate())
tableView.resize(550, 160)
tableView.show()
rowCount = 3
columnCount = 4
data = [
[i for i in range(columnCount)]
for j in range(rowCount)
]
model = TableModel(data)
tableView.setModel(model)
sys.exit(app.exec_())
When I specify app.setStyle('fusion') in __main__, I get what I would expect: When a cell in the column with the Delegate is selected, the cell background is blue and the image appears in front of it:
However, if I change to app.setStyle('windows'), even though in general it uses the same blue background for selected cells, when I move to a cell in the first column, the background disappears:
(You can't obviously see it, but the same cell is selected as in the first example).
That's just a piece of test code, which I don't completely understand.
In the actual application I'm writing, I am using Qt Designer to create the UI. Even though I specify app.setStyle('fusion'), the table has entirely different styling, with a different appearance to the background of a selected cell:
I can't for the life of me figure out where it is picking up the different style. It must come from Qt Designer somehow, but I've looked at the .py file Qt Designer creates, and I can't find it.
This style (wherever it comes from) seems to suffer from the same problem as the windows style. In the image above, there is no Delegate in use. The cell in row 2/column 2 is selected, and the background shows.
But if I add a Delegate to display a QPixmap in column 2, then the background does not show when the cell is selected:
(It's selected; take my word for it).
I thought maybe it was the case that once you use a Delegate to display an image, you could no longer get a background in the selected cell. But you obviously can. It works in one case, just not the others.
If anyone can shed light on this, I'd appreciate it. (I realize this is long; thanks for sticking with me).
I've been fiddling around with this issue more, and I've learned some things about my original question. In retrospect, I think it was not as clear as it could have been (or maybe I just understand it all a bit better).
For starters, I never should have referred to cells as being "selected". In fact, I don't even have the Qt.ItemIsSelectable flag set for any of the cells in the view. What I really have been trying to do is control the background of a cell when it is active (for lack of a better word) -- meaning it is the cell where the cursor is currently positioned.
This can be done by overriding initStyleOption() in the Delegate. My original test code is modified as shown below:
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = [[]], headers = None, parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__data = data
def rowCount(self, parent):
return len(self.__data)
def columnCount(self, parent):
return len(self.__data[0])
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
value = self.__data[row][column]
return value
if role == QtCore.Qt.BackgroundRole:
return QtGui.QBrush(QtGui.QColor(255, 255, 255))
def flags(self, index):
return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable
# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):
# <Modification>
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if (
index.row() == tableView.currentIndex().row() and
index.column() == tableView.currentIndex().column()
):
option.backgroundBrush = QtGui.QBrush(QtGui.QColor(232, 244, 252))
def paint(self, painter, option, index):
if (index.column() == 0):
# <Modification>
if (
index.row() == tableView.currentIndex().row() and
index.column() == tableView.currentIndex().column()
):
self.initStyleOption(option, index)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(option.backgroundBrush)
painter.drawRect(option.rect)
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
else:
super().paint(painter, option, index)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
tableView = TableView()
tableView.resize(550, 160)
tableView.setItemDelegate(Delegate())
tableView.show()
rowCount = 3
columnCount = 4
data = [
[i for i in range(columnCount)]
for j in range(rowCount)
]
model = TableModel(data)
tableView.setModel(model)
sys.exit(app.exec_())
initStyleOption() sets the background brush for a cell when it is active (current). But as I bemoaned before, this doesn't occur in the first column, which has a Delegate with a custom paint() method that displays a pixmap. So paint() must also take responsibility for setting the background for cells in that column when they are active. It uses the same backgroundBrush that initStyleOption() set.
The result is very nearly what I'm shooting for. The only fly in the ointment is that there is still clearly additional styling going on, that affects all the cells in the view except those in column 1 with the custom Delegate. So they don't look quite exactly alike when active:
(It's subtle, but there's a bit of a gradient to the background of the cell in column 2, which is absent in column 1).
I know now that there are style 'factories' that apply a widget-wide style. Since I'm using Fusion, that is evidently where the extra styling is coming from.
So now my question is -- where is that styling defined, and do I have any control over it? If I could see it, I could make my custom background style match it. Better yet, if I could modify it, I could make it match mine.
I had the same problem with my own tool today. I think your issue is the same as this other question. In short, you just need to call super in paint before doing any of your extra work. When I added super to my own code, selections worked again as expected in the delegate.
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
if (index.column() == 0):
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
(FWIW I haven't tested the code above. But it should work).

How to create a dropdown from a gtk.Entry's icon?

I have a gtk.Entry with an icon after the text, intending to be a text search field:
What I'm trying to do is to display a dropdown (i.e. a gtk.ComboBox) when the user clicks on the icon, to choose the type of search. A mock of that feature would be:
I have tried several things without any success. For example, trying to pack an empty gtk.ComboBox only showing an arrow right after the Entry, and stuffing it only on icon-press, which creates the illusion, but it has two drawbacks: a) when I stuff the ComboBox, the toolbar grows, and b) when I clear() the ListStore, the ComboBox retains its width and leaves an ugly grey box.
At this point I guess that I need to create a CellRenderer on icon-press that pops down the icon of the Entry, and I tried without a lot of success to understand the code of gtk.ComboBoxEntry (in gtkcomboboxentry.c), but as far as I understood it uses a vertical Container on the whole piece together with a CellRenderer.
Also GTK+3 doesn't have any ideas on this respect.
Any ideas, or some guidance in how to create this in PyGTK?
I was looking for something similar, so I came up with the code below. I haven't really worried about the aesthetics. I did pass a list of tuples to the MyPopup class, with the idea of passing handlers for each of the menu items in the dropdown. Note that the item.show() is necessary, even though there is a show_all():
from gi.repository import Gtk
class MyPopup(Gtk.MenuButton):
def __init__(self, btndefs):
super(MyPopup, self).__init__()
self.menu = Gtk.Menu()
self.set_popup(self.menu)
#self.set_label(">")
self.set_direction(Gtk.ArrowType.RIGHT)
for btndef in btndefs:
item = Gtk.MenuItem()
item.set_label(btndef[0])
item.show()
self.menu.append(item)
class MainWindow(Gtk.Window):
def __init__(self):
super(MainWindow, self).__init__()
self.set_size_request(100, -1)
self.connect("destroy", lambda x: Gtk.main_quit())
self.hbox = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL)
self.entry = Gtk.Entry()
self.popup = MyPopup( (("String",),
("String no case",),
("Hexadecimal",),
("Regexp",)) )
self.hbox.pack_start(self.entry, True, True, 0)
self.hbox.pack_start(self.popup, False, True, 0)
self.add(self.hbox)
self.show_all()
def run(self):
Gtk.main()
def main():
mw = MainWindow()
mw.run()
return 0
if __name__ == '__main__':
main()
yup its year late, but lets not make next person stumbled here to be sad like me.
this is the example using Gtk.Menu() popup, you can also similar feat. with Gtk.Popover()
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
opts = {
'hex' : "system-run-symbolic",
'regex' : "font-select-symbolic",
'string' : "font-x-generic-symbolic",
'no-case' : "tools-check-spelling-symbolic",
}
def make_menu(entry, opts):
menu = Gtk.Menu()
for label, icon in opts.items():
item = Gtk.MenuItem()
item.set_label(label)
item.connect(
"activate",
lambda w: entry.set_icon_from_icon_name(0, opts[w.get_label()])
)
menu.append(item)
# NOTE you can use Gtk.ImageMenuItem to add image but its
# Deprecated since version 3.10
menu.show_all()
return menu
def on_icon_release(widget, pos, event):
menu = make_menu(widget, opts)
menu.popup(
parent_menu_shell = None,
parent_menu_item = None,
func = None,
data = None,
button = Gdk.BUTTON_PRIMARY,
activate_time = event.get_time()
)
def make_entry():
entry = Gtk.Entry()
entry.set_icon_from_icon_name(0, 'action-unavailable-symbolic')
entry.set_icon_from_icon_name(1, 'fonts')
entry.set_icon_sensitive(1, True)
entry.set_icon_activatable(1, True)
entry.connect("icon-release", on_icon_release)
return entry
root = Gtk.Window()
root.add(make_entry())
root.show_all()
Gtk.main()

wxPython - drawing on transparent/alpha background (for custom widgets/panels)

I'm learning wxPython on Ubuntu Linux - and I would like to define my own widget, which is basically a line, which I'd like to move around the window.. I'm getting somewhere, but the problem is that I cannot get the 'widget' to 'draw' on a transparent background; best I can get is something like this (the yellow line should be an independent widget with a transparent background - but the background there is black with noise):
The code I came up with is below. I don't want the whole window transparent (wxpython - Python drawing on screen - Stack Overflow); I'm aware wx.TRANSPARENT is only for text, and I should try wx.GCDC, which I did, but it isn't working (wx.PaintDC and SetBackgroundMode( wx.TRANSPARENT ) support - wxPython-users | Google Groups), and apparently, this, on "wxGTK it is not possible" (wxPython-users - transparent background for a panel widget)...
It seems the only way would be to use a transparent bitmap/Image, and then use that as background for a custom widget, would that be correct? If so, is there a possibility to generate this bitmap/image directly in wxPython (I'm aiming for a self-contained script, I'd hate to make it dependent on an external .png :)) ? And if this is a possible approach, can someone point me to a minimal working example (as I cannot find any examples for this kind of use at all)..
Thanks in advance for any help,
Cheers!
code that generated image above:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import wx
class CustomLine(wx.Panel): #PyControl
"""
A custom class for a line
Modified from http://wiki.wxpython.org/CreatingCustomControls
"""
def __init__(self, parent, id=wx.ID_ANY, label="", pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator,
name="CustomLine"):
"""
Default class constructor.
#param parent: Parent window. Must not be None.
#param id: CustomLine identifier. A value of -1 indicates a default value.
#param label: Text to be displayed next to the checkbox.
#param pos: CustomLine position. If the position (-1, -1) is specified
then a default position is chosen.
#param size: CustomLine size. If the default size (-1, -1) is specified
then a default size is chosen.
#param style: not used in this demo, CustomLine has only 2 state
#param validator: Window validator.
#param name: Window name.
"""
#~ wx.PyControl.__init__(self, parent, id, pos, size, style, validator, name)
wx.Panel.__init__(self, parent, id, pos, size, style)
# Bind the events related to our control: first of all, we use a
# combination of wx.BufferedPaintDC and an empty handler for
# wx.EVT_ERASE_BACKGROUND (see later) to reduce flicker
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.lpen = wx.Pen('yellow', 2, wx.SOLID)
self.imagebkg = wx.EmptyImage( 10, 10 )
#~ self.imagebkg.SetData((255,255,255))
#~ self.imagebkg.SetAlphaData((1))
def OnPaint(self, event):
""" Handles the wx.EVT_PAINT event for CustomLine. """
# If you want to reduce flicker, a good starting point is to
# use wx.BufferedPaintDC.
pdc = wx.BufferedPaintDC(self)
dc = wx.GCDC(pdc)
# Is is advisable that you don't overcrowd the OnPaint event
# (or any other event) with a lot of code, so let's do the
# actual drawing in the Draw() method, passing the newly
# initialized wx.BufferedPaintDC
self.Draw(dc)
def Draw(self, dc):
"""
Actually performs the drawing operations, for the bitmap and
for the text, positioning them centered vertically.
"""
# Get the actual client size of ourselves
width, height = self.GetClientSize()
if not width or not height:
# Nothing to do, we still don't have dimensions!
return
# Initialize the wx.BufferedPaintDC, assigning a background
# colour and a foreground colour (to draw the text)
#~ backColour = self.GetBackgroundColour()
#~ backBrush = wx.Brush((1,1,1,150), wx.TRANSPARENT) # backColour
#~ backBrush = wx.Brush((10,10,1,150)) # backColour
dc.SetBackground(wx.TRANSPARENT_BRUSH) #() backBrush
#~ dc.SetBackgroundMode(wx.TRANSPARENT)
dc.Clear()
dc.SetPen(self.lpen)
dc.DrawLine(0, 0, 100, 100)
def OnEraseBackground(self, event):
""" Handles the wx.EVT_ERASE_BACKGROUND event for CustomLine. """
# This is intentionally empty, because we are using the combination
# of wx.BufferedPaintDC + an empty OnEraseBackground event to
# reduce flicker
pass
class MyTestFrame(wx.Frame):
def __init__(self, parent, title):
super(MyTestFrame, self).__init__(parent, title=title,
size=(250, 150))
# the master panel of the frame - "Add a panel so it looks correct on all platforms"
self.panel = wx.Panel(self, wx.ID_ANY)
# self.panel.SetBackgroundColour(wx.Colour(124, 224, 124)) # to confirm the square is the panel
self.mpanelA = wx.Panel(self.panel, -1, size=(200,50))
self.mpanelA.SetBackgroundColour((200,100,200))
self.mpanelB = wx.Panel(self.panel, -1, size=(50,200), pos=(50,30))
self.mpanelB.SetBackgroundColour(wx.Colour(200,100,100,100))
self.cline = CustomLine(self.panel, -1, size=(-1,200))
self.Centre()
self.Show()
if __name__ == '__main__':
app = wx.App()
MyTestFrame(None, 'Test')
app.MainLoop()
maybe you should have a look at GraphicsContext istead of dc (DrawingContext). It has better support for transparency, like drawing transparent rectangles on to of a panel.