I have built a graphic oriented package using the Graph element. I need to do keyboard input based on Graph Element coordinates. Currently I am using the events that come in from the keyboard to place characters on the Graph element using draw_text. It works but is a bit slow and I get into problems with interpreting the key codes I get back from different platforms and the overhead with me doing the echoing back on to the Graph element does not help.
My Question. In PySimpleGui(Tk) is there a way to use the Tk Entry function directly on Graph Coordinates?
IMO, it can be done like your request, but much complex.
Here only a simple way to enter text on a Graph element.
import PySimpleGUI as sg
font = ('Courier New', 16, 'bold')
layout = [
[sg.Input(expand_x=True, key='INPUT')],
[sg.Graph((320, 240), (0, 0), (320, 240), enable_events=True, key='GRAPH',
background_color='green')],
]
window = sg.Window('Draw Text', layout, margins=(0, 0), finalize=True)
entry, graph = window['INPUT'], window['GRAPH']
while True:
event, values = window.read()
if event == sg.WIN_CLOSED:
break
elif event == 'GRAPH':
location, text = values['GRAPH'], values['INPUT']
if text:
graph.draw_text(text, location, font=font, color='white', text_location=sg.TEXT_LOCATION_LEFT)
window.close()
Here, something like your request, but much complex and tkinter code required.
import PySimpleGUI as sg
def entry_callback(event, graph, entry_id, font, location, color):
widget = event.widget
text = widget.get()
graph.widget.delete(entry_id)
if text:
graph.draw_text(text, location=location, font=font, color=color, text_location='sw')
font = ('Courier New', 16, 'bold')
layout = [
[sg.Graph((320, 240), (0, 0), (320, 240), enable_events=True, key='GRAPH',
background_color='green')],
]
window = sg.Window('Draw Text', layout, margins=(0, 0), finalize=True)
graph = window['GRAPH']
while True:
event, values = window.read()
if event == sg.WIN_CLOSED:
break
elif event == 'GRAPH':
location = tuple(map(int, graph._convert_xy_to_canvas_xy(*values['GRAPH'])))
entry = sg.tk.Entry(graph.widget, font=font, fg='white', bg='green', width=45)
entry_id = graph.widget.create_window(*location, window=entry, anchor="sw")
entry.bind('<Return>', lambda event, graph=graph, entry_id=entry_id, font=font, location=values['GRAPH'], color='white':entry_callback(event, graph, entry_id, font, location, color))
entry.focus_force()
window.close()
Related
I am adding a QGraphicTextItem to a scene using pyqt6.
I cannot resize the widget border when text is resized.
I have looked at a few way of resizing, but none work.
The text does change to a bigger font via the context menu.
The entire class is shown below.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super(FreeTextGraphicsItem, self).__init__(None)
self.x = x
self.y = y
self.text = text_
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(self.font)
self.setPlainText(self.text)
self.setPos(self.x, self.y)
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
menu.addAction(_("Large font"))
action = menu.exec(QtGui.QCursor.pos())
if action is None:
return
if action.text() == "Large font":
self.font = QtGui.QFont(self.settings['font'], 12, QtGui.QFont.Weight.Normal)
frame = self.document().documentLayout().frameBoundingRect(self.document().rootFrame())
self.boundingRect().setRect(0, 0, frame.width(), frame.height())
def paint(self, painter, option, widget):
color = QtCore.Qt.GlobalColor.white
painter.setBrush(QtGui.QBrush(color, style=QtCore.Qt.BrushStyle.SolidPattern))
painter.drawRect(self.boundingRect())
painter.setFont(self.font)
fm = painter.fontMetrics()
painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.black))
lines = self.text.split('\\n')
for row in range(0, len(lines)):
painter.drawText(5, fm.height() * (row + 1), lines[row])
You're not using the features of QGraphicsTextItem.
In fact, you're completely ignoring and overriding most of its aspects:
x and y are existing and dynamic properties of all QGraphicsItems and should never be overwritten;
the same for font of QGraphicsTextItem;
calling setRect() on the bounding rectangle is useless, as boundingRect() is a *property getter" and is returned internally by the item based on its contents (in this case, the text set with setPlainText());
the text drawing is completely overridden, and not reliable nor consistent with the text set for the item, considering that you're painting the text with split lines, while the original text has escaped new lines;
If your main purpose is to draw a border around the item, then you should only do that, and then rely on the existing capabilities of the item.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super().__init__(text_.replace('\\n', '\n'))
self.setPos(x, y)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
)
font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(font)
self.setDefaulTextColor(QtGui.QColor(QtCore.Qt.GlobalColor.white))
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
largeFontAction = menu.addAction(_("Large font"))
action = menu.exec(event.screenPos())
if action == largeFontAction:
font = QtGui.QFont(
self.settings['font'], 12, QtGui.QFont.Weight.Normal)
self.setFont(font)
def paint(self, painter, option, widget=None):
painter.save()
painter.setBrush(QtCore.Qt.GlobalColor.white)
painter.drawRect(self.boundingRect())
painter.restore()
super().paint(painter, option, widget)
Note: comparing actions with their text is pointless, other than conceptually wrong; not only you can have a more reliable object-based comparison using the action (as shown above), but that comparison can also become invalid: a menu could contain items that have the same names, and you're also probably using the _ for translations, so the text might not match at all.
I'm working on line plotting a metric for a course module as well as each of its questions within a Jupyter Notebook using %matplotlib notebook. That part is no problem. A module has typically 20-35 questions, so it results in a lot of lines on a chart. Therefore, I am plotting the metric for each question in a low alpha and I want to change the alpha and display the question name when I hover over the line, then reverse those when no longer hovering over the line.
The thing is, I've tried every test version of interactivity from the matplotlib documentation on event handling, as well as those in this question. It seems like the mpl_connect event is never firing, whether I use click or hover.
Here's a test version with a reduced dataset using the solution to the question linked above. Am I missing something necessary to get events to fire?
def update_annot(ind):
x,y = line.get_data()
annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
text = "{}, {}".format(" ".join(list(map(str,ind["ind"]))),
" ".join([names[n] for n in ind["ind"]]))
annot.set_text(text)
annot.get_bbox_patch().set_alpha(0.4)
def hover(event):
vis = annot.get_visible()
if event.inaxes == ax:
cont, ind = line.contains(event)
if cont:
update_annot(ind)
annot.set_visible(True)
fig.canvas.draw_idle()
else:
if vis:
annot.set_visible(False)
fig.canvas.draw_idle()
module = 'bd2bc472-ee0d-466f-8557-788cc6de3018'
module_metrics[module] = {
'q_count': 31,
'sequence_pks': [0.5274546300604932,0.5262044653349001,0.5360993905297703,0.5292329279700655,0.5268691588785047,0.5319099014547161,0.5305164319248826,0.5268235294117647,0.573648805381582,0.5647933116581514,0.5669839795681448,0.5646591970121382,0.5663157894736842,0.5646976090014064,0.5659005628517824,0.5693634879925391,0.5728268468888371,0.5668834184858337,0.5687237026647967,0.5795640965549567,0.5877684407096172,0.585690904839841,0.5766899766899767,0.5971341320178529,0.6059972105997211,0.6055516678329834,0.6209865053513262,0.6203121360354065,0.6153666510976179,0.6236909471724459,0.6387654898293196],
'q_pks': {
'0da04f02-4aad-4ac8-91a5-214862b5c0d0': [0.6686046511627907,0.6282051282051282,0.76,0.6746987951807228,0.7092198581560284,0.71875,0.6585365853658537,0.7070063694267515,0.7171052631578947,0.7346938775510204,0.7737226277372263,0.7380952380952381,0.6774193548387096,0.7142857142857143,0.7,0.6962962962962963,0.723404255319149,0.6737588652482269,0.7232704402515723,0.7142857142857143,0.7164179104477612,0.7317073170731707,0.6333333333333333,0.75,0.7217391304347827,0.7017543859649122,0.7333333333333333,0.7641509433962265,0.6869565217391305,0.75,0.794392523364486],
'10bd29aa-3a26-49e6-bc2c-50fd503d7ab5': [0.64375,0.6014492753623188,0.5968992248062015,0.5059523809523809,0.5637583892617449,0.5389221556886228,0.5576923076923077,0.51875,0.4931506849315068,0.5579710144927537,0.577922077922078,0.5467625899280576,0.5362318840579711,0.6095890410958904,0.5793103448275863,0.5159235668789809,0.6196319018404908,0.6143790849673203,0.5035971223021583,0.5897435897435898,0.5857142857142857,0.5851851851851851,0.6164383561643836,0.6054421768707483,0.5714285714285714,0.627906976744186,0.5826771653543307,0.6504065040650406,0.5864661654135338,0.6333333333333333,0.6851851851851852]
}}
suptitle_size = 24
title_size = 18
tick_size = 12
axis_label_size = 15
legend_size = 14
fig, ax = plt.subplots(figsize=(15,8))
fig.suptitle('PK by Sequence Order', fontsize=suptitle_size)
module_name = 'Test'
q_count = module_metrics[module]['q_count']
y_ticks = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]
x_ticks = np.array([x for x in range(0,q_count)])
x_labels = x_ticks + 1
# Plot it
ax.set_title(module_name, fontsize=title_size)
ax.set_xticks(x_ticks)
ax.set_yticks(y_ticks)
ax.set_xticklabels(x_labels, fontsize=tick_size)
ax.set_yticklabels(y_ticks, fontsize=tick_size)
ax.set_xlabel('Sequence', fontsize=axis_label_size)
ax.set_xlim(-0.5,q_count-0.5)
ax.set_ylim(0,1)
ax.grid(which='major',axis='y')
# Output module PK by sequence
ax.plot(module_metrics[module]['sequence_pks'])
# Output PK by sequence for each question
for qid in module_metrics[module]['q_pks']:
ax.plot(module_metrics[module]['q_pks'][qid], alpha=0.15, label=qid)
annot = ax.annotate("", xy=(0,0), xytext=(-20,20),textcoords="offset points", bbox=dict(boxstyle="round", fc="w"), arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)
mpl_id = fig.canvas.mpl_connect('motion_notify_event', hover)
Since there are dozens of modules, I created an ipywidgets dropdown to select the module, which then runs a function to output the chart. Nonetheless, whether running it hardcoded as here or from within the function, mpl_connect never seems to fire.
Here's what this one looks like when run
The Gtk.TreeView implements a default drag icon. It use the background color of the TreeView, it's font and the complete row-content as string.
I want the same (background-color, font-face, font-size, font-color) but with a shorter string (only the second of three columns).
In the example below create my own cairo.Surface to create such an icon. But color and font is a problem. I don't know how to set them up or (much more important) to ask the TreeView or Gtk itself for the current color and font values.
How does the TreeView get this values?
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
import cairo
class MainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="TreeView Drag and Drop")
self.connect("delete-event", Gtk.main_quit)
self.box = Gtk.Box()
self.add(self.box)
# "model" with dummy data
self.store = Gtk.TreeStore(int, str, int)
for i in range(5):
self.store.append(None, [i, 'Item {}'.format(i), i]) # treeview
self.tree = Gtk.TreeView(model=self.store)
self.box.pack_start(self.tree, True, True, 0)
# build columns
colA = Gtk.TreeViewColumn('Col A', Gtk.CellRendererText(), text=0)
self.tree.append_column(colA)
colB = Gtk.TreeViewColumn('Col B', Gtk.CellRendererText(), text=1)
self.tree.append_column(colB)
colC = Gtk.TreeViewColumn('Col C', Gtk.CellRendererText(), text=2)
self.tree.append_column(colC)
# enable default drag and drop
self.tree.set_reorderable(True)
# DnD events
self.tree.connect_after("drag-begin", self.drag_begin)
def drag_begin(self, widget, context):
model, path = widget.get_selection().get_selected_rows()
text = model[path][1]
# dummy surface/context
surface = cairo.ImageSurface(cairo.Format.RGB24, 0, 0)
cr = cairo.Context(surface)
# calculate text size
txtext = cr.text_extents(text)
width = int(txtext.width)
height = int(txtext.height)
offset = 10
# creal surface/context
surface = cairo.ImageSurface(cairo.Format.RGB24,
width + (offset*2),
height + (offset*2))
cr = cairo.Context(surface)
cr.set_source_rgb(1, 1, 1) # text color: white
cr.move_to(0+offset, height+offset)
cr.show_text(text)
# use the surface as drag icon
Gtk.drag_set_icon_surface(context, surface)
win = MainWindow()
win.show_all()
Gtk.main()
What I tried (but not worked) was cairo.Surface.create_similar()',cairo.Surface.create_similar_image()andGtk.TreeView.create_row_drag_icon()`.
This answer is based on a foreign mailing list posting.
The widget has a Gtk.StyleContext. A Pango.Layout is used to render the text based on the style informations in the Gtk.StyleContext.
def drag_begin(self, widget, context):
model, path = widget.get_selection().get_selected_rows()
text = model[path][1]
stylecontext = widget.get_style_context()
# new pango layout
pl = widget.create_pango_layout(text)
ink_rec, log_rect = pl.get_pixel_extents()
padding = 5
# create surface/context
surface = cairo.ImageSurface(cairo.Format.RGB24,
log_rect.width + (padding*2),
log_rect.height + (padding*2))
cr = cairo.Context(surface)
Gtk.render_background(stylecontext, cr, 0, 0,
log_rect.width + (padding*2),
log_rect.height + (padding*2))
Gtk.render_layout(stylecontext, cr, padding, padding, pl)
# border
line_width = cr.get_line_width()
cr.rectangle(-1+line_width, -1+line_width,
log_rect.width+(padding*2)-line_width,
log_rect.height+(padding*2)-line_width)
cr.stroke()
# use the surface as drag icon
Gtk.drag_set_icon_surface(context, surface)
class RangeSelection(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QGridLayout(self)
self.setLayout(layout)
self._create_widgets()
layout.addWidget(self.select_combo, 1, 1)
layout.addWidget(self.stacked, 1, 2, 5, 1)
self.stacked.currentWidget().setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred)
self.stacked.currentChanged.connect(self.onCurrentChanged)
def onCurrentChanged(self):
currentw = self.stacked.currentWidget()
currentw.adjustSize()
if currentw == self.releasew:
currentw.sizeAdjustPolicy = QComboBox.AdjustToContentsOnFirstShow
self.adjustSize()
def _create_widgets(self):
self.stacked = QStackedWidget()
self.datew = QCalendarWidget()
self.datew.setVerticalHeaderFormat(QCalendarWidget.
NoVerticalHeader)
self.stacked.addWidget(self.datew)
self.buildidw = QLineEdit()
self.stacked.addWidget(self.buildidw)
self.releasew = QComboBox()
self.releasew.addItems([str(k) for k in sorted(releases())])
self.stacked.addWidget(self.releasew)
self.revw = QLineEdit()
self.stacked.addWidget(self.revw)
self.select_combo = QComboBox()
self.select_combo.addItems(['date', 'buildid', 'release', 'changeset'])
self.select_combo.activated.connect(self.stacked.setCurrentIndex)
I have this code where I am having four widgets in the QStackedWidget. When I run this code and change my selection in self.select_combo from date to release, the self.releasew combobox initially shows up as same size as that of the QCalendarWidget( which obviously looks horrible ). But, when I change my selection from release to any other value and then back to release, the self.releasew combobox shows up in the size it should. Why is this happening? What is the solution to this problem?
Note: I am using PyQt4. Also note that widgets for buildid and changeset do not show any abnormal behaviour.
I removed the setSizePolicy and sizeAdjustPolicy code. I also removed the call to self.adjustSize(). This worked. Though, I don't know why.
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.