Im currently working on a little graphics library for pyglet. The module is used to make vector graphics composed entirely of lines.
In its develoment i have have run into a problem where, clicking to drag (and move a point) also causes fires an on_mouse_press event that creates a new link between the last active link and the point you are trying to drag.
I cant seem to think of any way to fix this that dosent make creating links between points feel laggy, i have creating links on_mouse_release instead so that i could determine if the mouse had been draged before linking the point.
Does anyone else have any bright ideas on how i can get this to work without appearing laggy.
EDIT: to clarify im using pyglet with python
#!/usr/bin/python
import pyglet
from time import time, sleep
class Window(pyglet.window.Window):
def __init__(self, refreshrate):
super(Window, self).__init__(vsync = False)
self.frames = 0
self.framerate = pyglet.text.Label(text='Unknown', font_name='Verdana', font_size=8, x=10, y=10, color=(255,255,255,255))
self.last = time()
self.alive = 1
self.refreshrate = refreshrate
self.click = None
self.drag = False
def on_draw(self):
self.render()
def on_mouse_press(self, x, y, button, modifiers):
self.click = x,y
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
if self.click:
self.drag = True
print 'Drag offset:',(dx,dy)
def on_mouse_release(self, x, y, button, modifiers):
if not self.drag and self.click:
print 'You clicked here', self.click, 'Relese point:',(x,y)
else:
print 'You draged from', self.click, 'to:',(x,y)
self.click = None
self.drag = False
def render(self):
self.clear()
if time() - self.last >= 1:
self.framerate.text = str(self.frames)
self.frames = 0
self.last = time()
else:
self.frames += 1
self.framerate.draw()
self.flip()
def on_close(self):
self.alive = 0
def run(self):
while self.alive:
self.render()
# ----> Note: <----
# Without self.dispatc_events() the screen will freeze
# due to the fact that i don't call pyglet.app.run(),
# because i like to have the control when and what locks
# the application, since pyglet.app.run() is a locking call.
event = self.dispatch_events()
sleep(1.0/self.refreshrate)
win = Window(23) # set the fps
win.run()
Short description:
Keep track of the current mouse state within the window
Keep track of startpoint of mouse event
At mouse release, do waht you want to do or update whatever it is within the drag.
I know it's an old question, but it's under the "unanswered" so hoping to get it resolved and out of the list.
On MouseDown set a boolean variable to indicate that other events should be ignored.
private bool IgnoreEvents { set; get; }
...
protected void Control_MouseDown(object sender, EventArgs e)
{
//Dragging or holding.
this.IgnoreEvents = true;
}
protected void Control_MouseUp(object sender, EventArgs e)
{
//Finished dragging or holding.
this.IgnoreEvents = false;
}
protected void OtherControl_SomeOtherEvent(object sender, EventArgs e)
{
if (this.IgnoreEvents)
return;
//Otherwise to normal stuff here.
}
You'll need to tweak this to work with your code of course but it should be a good start.
Related
I'm trying to implement a button in PyQt5 which acts identically to pressing the Tab-Key. For this, I get the focused item, and call nextInFocusChain() to get the next item in tab order and set it to focus. If it is not a QLineEdit object, I repeat.
self.focusWidget().nextInFocusChain().setFocus()
while type(self.focusWidget()) != QLineEdit:
print(str(self.focusWidget()) + "" + str(self.focusWidget().nextInFocusChain()))
self.focusWidget().nextInFocusChain().setFocus()
Sadly this snipped does not wrap around. After the last QLineEdit, it gets stuck inside the loop while continuously printing
...
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
Here a reproducing example...
from collections import OrderedDict
from PyQt5.QtWidgets import QDialog, QFormLayout, QLineEdit, QCheckBox, QDialogButtonBox, QVBoxLayout, QPushButton
class AddressEditor(QDialog):
def __init__(self,fields:OrderedDict,parent=None):
super(AddressEditor, self).__init__(parent=parent)
layout = QVBoxLayout(self)
form = QFormLayout(self)
self.inputs = OrderedDict()
last = None
for k,v in fields.items():
self.inputs[k] = QLineEdit(v)
if last is not None:
self.setTabOrder(self.inputs[last],self.inputs[k])
last = k
form.addRow(k, self.inputs[k])
layout.addLayout(form)
button = QPushButton("TAB")
layout.addWidget(button)
button.clicked.connect(self.tabpressed)
def tabpressed(self):
self.focusWidget().nextInFocusChain().setFocus()
while type(self.focusWidget()) != QLineEdit:
print(str(self.focusWidget()) + "" + str(self.focusWidget().nextInFocusChain()))
self.focusWidget().nextInFocusChain().setFocus()
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
dialog = AddressEditor({"Text":"Default","Empty":"","True":"True","False":"False"})
if dialog.exec():
pass
exit(0)
There are two important aspects to consider when trying to achieve "manual" focus navigation:
the focus chain considers all widgets, not only those that can "visibly" accept focus; this includes widgets that have a NoFocus policy, the parent widget (including the top level window), hidden widgets, and any other widget that can be "injected" by the style, including "helpers" like QFocusFrame;
widgets can have a focus proxy which can "push back" the focus to the previous widget, and this can cause recursion issues like in your case;
Besides that, there are other issues with your implementation:
the button accepts focus, so whenever it's pressed it resets the focus chain;
to compare the class you should use isinstance and not type;
form should not have any argument since it's going to be added as a nested layout, and in any case it shouldn't be self since a layout has already been set;
the tab order must be set after both widgets are added to parents that share the same a common ancestor widget/window;
the tab order is generally automatically set based on when/where widgets are added to the parent: for a form layout is generally unnecessary as long as all fields are inserted in order;
class AddressEditor(QDialog):
def __init__(self, fields:OrderedDict, parent=None):
super(AddressEditor, self).__init__(parent=parent)
layout = QVBoxLayout(self)
form = QFormLayout() # <- no argument
layout.addLayout(form)
self.inputs = OrderedDict()
last = None
for k, v in fields.items():
new = self.inputs[k] = QLineEdit(v)
form.addRow(k, new) # <- add the widget *before* setTabOrder
if last is not None:
self.setTabOrder(last, new)
last = new
button = QPushButton("TAB")
layout.addWidget(button)
button.clicked.connect(self.tabpressed)
button.setFocusPolicy(Qt.NoFocus) # <- disable focus for the button
def tabpressed(self):
nextWidget = self.focusWidget().nextInFocusChain()
while not isinstance(nextWidget, QLineEdit) or not nextWidget.isVisible():
nextWidget = nextWidget.nextInFocusChain()
nextWidget.setFocus()
If you want to keep the focus policy for the button so that it can be reached through Tab, the only possibility is to keep track of the focus change of the application, since as soon as the button is pressed with the mouse button it will already have received focus:
class AddressEditor(QDialog):
def __init__(self, fields:OrderedDict, parent=None):
# ...
button = QPushButton("TAB")
layout.addWidget(button)
button.clicked.connect(self.tabpressed)
QApplication.instance().focusChanged.connect(self.checkField)
self.lastField = tuple(self.inputs.values())[0]
def checkField(self, old, new):
if isinstance(new, QLineEdit) and self.isAncestorOf(new):
self.lastField = new
def tabpressed(self):
nextWidget = self.lastField.nextInFocusChain()
while not isinstance(nextWidget, QLineEdit) or not nextWidget.isVisible():
nextWidget = nextWidget.nextInFocusChain()
nextWidget.setFocus()
Here is the class NPC:
class NPC:
def __init__(self):
self.x = randint(0,800)
self.y = randint(0,60)
self.velocity_x = 5
self.drop_y = 60
self.img = "my image path here"
npc = NPC()
num_npc = 5
list = []
for i in range(num_npc):
list.append(npc)
In the game loop only one image is shown and is stationary.
I'm working on trying to write old code to be object oriented and can't figure out the best way to render the npcs
Below is the old code I was using and it worked as expected
npc_img = []
npc_x = []
npc_y = []
npc_vel_x = []
npc_vel_y = []
num_of_npc = 5
for i in range(num_of_npc):
npc_img.append("my img path")
npc_x.append(random.randint(0, 800))
npc_y.append(random.randint(0, 60))
npc_vel_x.append(4)
npc_vel_y.append(40)
Your code is pretty much correct already. However the way you are creating instances of NPC objects is not quite correct. I guess you meant to add 5 NPCs to the list, not 5 references to the same NPC object. That is what your question title says though!
npc = NPC()
...
for i in range(num_npc):
list.append(npc) # <<-- HERE, same object, 5 times
The code should call the NPC constructor in the loop, rather than outside it.
for i in range( num_npc ):
new_npc = NPC()
list.append( new_npc )
While you're rewriting code, it might be worth keeping the co-ordinates and image dimensions in a Pygame Rect, since this allows for easy collision detection and other nice things.
Something like:
class NPC:
def __init__(self):
self.image = pygame.image.load( "image path here" ).convert_alpha()
self.rect = self.image.get_rect()
self.rect.x = randint(0,800)
self.rect.y = randint(0,60)
self.velocity_x = 5
self.drop_y = 60
def draw( self, screen ):
screen.blit( self.image, self.rect )
def hitBy( self, arrow_rect ):
hit = self.rect.colliderect( arrow_rect )
return hit
If I understood correctly, something like this should work:
class NPC:
def __init__(self):
self.x = randint(0,800)
self.y = randint(0,60)
self.velocity_x = 5
self.drop_y = 60
self.image_list = []
self_image_load_dict = {}
def add_image(self, image_path):
self.image_list.append(image_path)
def load_images(self):
self.image_load_dict[]
for i in len(self.get_image_list()):
self.image_load_dict[i] = pygame.image.load(self.get_image_list()[i])
def get_image_list(self):
return self.image_list
def get_image_load_dict(self):
return self.image_load_dict
I used fstring so it would be easier to load images and keep track of the image number:
npc = NPC()
for i in range(NUMBER_OF_NPC):
npc.add_image(f"image_path_{i}")
Now you have image_paths in the object's list, I assume you want to load them, hence the load_images method.
NOTE: If needed you can create additional method(s) for loading images. E.g. if you have animations for "left" and "right" movement
I hope this answers your question, if I omitted something please say in comment.
SOLUTION
Remove the channel and associated code
Add a new update function inside the window class which takes the new shapes as a parameter
modify the initialisation of the class
call the update function
Modifications for the solution
Apologies, but the diff markdown doesn't seem to be displaying properly, hopefully you should still get an idea of how the solution works
Window class
class Window(Gtk.Window):
- __gsignals__ = {
- 'update_signal': (GObject.SIGNAL_RUN_FIRST, None,
- ())
- }
-
- def do_update_signal(self):
- print("UPDATE SIGNAL CALLED")
- self.shapes = self.shapes_channel.read()
- print("Num new shapes:", len(self.shapes))
- self.show_all()
in the class method init_ui
self.connect("delete-event", Gtk.main_quit)
+ self.show_all()
+ a = self.darea.get_allocation()
+ print (a.x, a.y, a.width, a.height)
+ self.img = cairo.ImageSurface(cairo.Format.RGB24, a.width, a.height)
a new class method update_shapes
+ def update_shapes(self, shapes):
+ self.shapes = shapes
+ cr = cairo.Context(self.img)
+ self.draw_background(cr)
+ for shape in self.shapes:
+ shape.draw(cr)
+ self.darea.queue_draw()
+ return True
Main code
- shapes_channel = Channel()
iter_num = 0
- def optimize(chan, prob, signaller):
+ def optimize(prob, signaller):
def print_iter_num(xk):
global iter_num
iter_num += 1
prob.update_positions(xk)
prob.update_grads(jacobian(xk))
new_shapes = convert_grid(prob.grid, building_size=1.0/GRID_SIZE)
- chan.write(new_shapes)
- signaller.emit("update_signal")
+ GLib.idle_add(signaller.update_shapes, new_shapes)
print("Iteration", iter_num, "complete...")
try:
sol = minimize(objective, x0, bounds = all_bounds, constraints=constraints, options={'maxiter': MAX_ITER, 'disp': True}, callback=print_iter_num, jac=jacobian)
prob.update_positions(sol.x)
except Exception as e:
print("ran into an error", e)
- window = new_window(shapes_channel=shapes_channel)
+ window = new_window()
- x = threading.Thread(target=optimize, args=(shapes_channel, optim_problem, window))
+ x = threading.Thread(target=optimize, args=(optim_problem, window))
x.start()
window.run()
QUESTION
Window class
import cairo
import gi
import math
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
class Line():
def __init__(self, start, end, thickness, colour):
self.start = start
self.end = end
self.thickness = thickness
self.colour = colour
def draw(self, cr):
cr.move_to(*self.start)
cr.line_to(*self.end)
cr.set_source_rgba(*self.colour)
cr.set_line_width(self.thickness)
cr.stroke()
class Polygon():
def __init__(self, points, line_colour, line_thickness, fill_colour=None):
self.points = points # points should be an iterable of points
self.line_colour = line_colour
self.line_thickness = line_thickness
self.fill_colour = fill_colour
def draw(self, cr):
cr.move_to(*self.points[0])
for point in self.points[1:]:
cr.line_to(*point)
cr.close_path()
cr.set_source_rgba(*self.line_colour)
cr.set_line_width(self.line_thickness)
cr.stroke()
if self.fill_colour is not None:
cr.move_to(*self.points[0])
for point in self.points[1:]:
cr.line_to(*point)
cr.close_path()
cr.set_source_rgba(*self.fill_colour)
cr.fill()
class Window(Gtk.Window):
__gsignals__ = {
'update_signal': (GObject.SIGNAL_RUN_FIRST, None,
())
}
def do_update_signal(self):
print("UPDATE SIGNAL CALLED")
self.shapes = self.shapes_channel.read()
print("Num new shapes:", len(self.shapes))
self.show_all()
def __init__(self, shapes_channel, window_size, background_colour=(1, 1, 1, 1), title="GTK window"):
super(Window, self).__init__()
self.width = window_size[0]
self.height = window_size[1]
self.background_colour = background_colour
self.title = title
self.shapes = []
self.shapes_channel = shapes_channel
self.init_ui()
def init_ui(self):
darea = Gtk.DrawingArea()
darea.connect("draw", self.on_draw)
self.add(darea)
self.set_title(self.title)
self.resize(self.width, self.height)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
def draw_background(self, cr: cairo.Context):
cr.scale(self.width, self.height)
cr.rectangle(0, 0, 1, 1) # Rectangle(x0, y0, x1, y1)
cr.set_source_rgba(*self.background_colour)
cr.fill()
def on_draw(self, wid, cr: cairo.Context):
self.draw_background(cr)
for shape in self.shapes:
shape.draw(cr)
def run(self):
Gtk.main()
def new_window(shapes_channel,
window_size=(1000, 1000),
background_colour=(1,1,1,1),
title="3yp"):
return Window(shapes_channel,
window_size=window_size,
background_colour=background_colour,
title=title)
I'm trying to run a window that can draw the shapes I've defined (Lines and Polygons).
It worked fine before when I supplied it a list of shapes and ran it at the end of my application
However, I am trying to add interactivity and have it redraw a list of shapes when the update_signal gets called and a list of new shapes get passed along the shapes_channel that is part of the constructor.
Main Code
Here is the relevant bits from my main code:
shapes_channel = Channel()
iter_num = 0
def optimize(chan, prob, signaller):
def print_iter_num(xk):
global iter_num
iter_num += 1
prob.update_positions(xk)
prob.update_grads(jacobian(xk))
new_shapes = convert_grid(prob.grid, building_size=1.0/GRID_SIZE)
chan.write(new_shapes)
signaller.emit("update_signal")
print("Iteration", iter_num, "complete...")
try:
sol = minimize(objective, x0, bounds = all_bounds, constraints=constraints, options={'maxiter': MAX_ITER, 'disp': True}, callback=print_iter_num, jac=jacobian)
prob.update_positions(sol.x)
except Exception as e:
print("ran into an error", e)
window = new_window(shapes_channel=shapes_channel)
x = threading.Thread(target=optimize, args=(shapes_channel, optim_problem, window))
x.start()
window.run()
As you can see:
A Channel() object is created, named shapes_channel
A new window is created, with the shapes_channel passed into the constructor via the intermediate function new_window.
This window is passed to the other thread so that the other thread
can emit the relevant signal ("update_signal")
The other thread is run
The window is run in the main thread I get the following console output:
UPDATE SIGNAL CALLED
Num new shapes: 31
Gdk-Message: 01:27:14.090: main.py: Fatal IO error 0 (Success) on X server :0.
From the console output, we can infer that the signal is called successfully, and the new shapes are passed to the window and stored correctly, but it fails on the line self.show_all().
This is an object that was working fine previously, and producing graphical output, and I can only think of 2 possible things that may have changed from the objects perspective:
The Channel object works as intended, but perhaps the mere presence of an object that is shared across threads throws the whole thing into disarray
Even though it's on the main thread, it doesn't like that there are other threads.
I would really appreciate some guidance on this maddening occurrence.
About your assumptions:
It is unclear if your channel object is possible to safely access from two threads.
The signal handler is executed in the thread that emits the signal.
My guess would be that it is the fact that you emit the signal from another thread that causes the issue.
You can solve this by using GLib.idle_add(your_update_func). Instead of calling your_update_func directly, a request is added to the Gtk main loop, which executes it when there are no more events to process, preventing any threading issues.
Read more here: https://wiki.gnome.org/Projects/PyGObject/Threading
I have been trying to build my skills in Python and am trying to create a risk style game.
I am not far in to it at the moment as I am trying to get to grips with classes and Tkinter.
My first trial is to create a series of buttons to take the place of the different countries. I then want these buttons to update the amount of armies on the country when they are clicked.
So far I have been able to get the map to generate from the class I have created and the buttons are clickable. When a button is clicked it updates the amount of armies but always for the last button.
How do I get it so that the button I click updates and not the last one?
Have I gone about this in entirely the wrong way?
from tkinter import *
import random
class territory:
def __init__ (self, country, player = "1", current_armies = 0, x=0, y=0):
self.country = country
self.current_armies = current_armies
self.player = player
self.y = y
self.x = x
def get_armies(self):
print(self.country + " has " + str( self.current_armies)+ " armies.")
def add_armies (self, armies):
self.current_armies += armies
def roll_dice (self, dice=1):
rolls = []
for i in range(0, dice):
rolls.append(random.randint(1,6))
rolls.sort()
rolls.reverse()
print (self.country + " has rolled " + str(rolls))
return rolls
def owner(self):
print (self.country + " is owned by " + self.player)
def get_country(self):
print(country)
def button (self):
Button(window, text = territories[0].current_armies, width = 10, command = click1(territories, 0)).grid(row=y,column=x)
window = Tk()
def create_territories():
countries = ["UK", "GER", "SPA", "RUS"]
terr_pos = [[1,0],[2,0],[1,5],[4,1]]
sta_arm = [1,1,1,1]
terr = []
player = "1"
for i in range(len(countries)):
terr.append(territory(countries[i],player, sta_arm [i] , terr_pos[i][0],terr_pos[i][1]))
if player == "1":
player = "2"
else:
player = "1"
return terr
def click1(territory, i):
territory[i].current_armies += 1
build_board(territory)
def build_board(territories):
for i in range(0,4):
Button(window, text = territories[i].country+"\n"+str(territories[i].current_armies), width = 10, command = lambda: click1(territories, i)).grid(row=territories[i].y,column=territories[i].x)
territories = create_territories()
window.title ("Domination")
create_territories()
build_board(territories)
window.mainloop()
In your def button(self):... you are always referencing territories[0]:
Button(window, text=territories[0].current_armies,... command=click1(territories, 0)...
As such, you are always using the first territory as your reference, so you ought to initialize each territory with its index in territories[] so you can pass that into your Button constructor.
On your question of "entirely the wrong way," I'd personally send that question over to CodeReview, since that's more of their domain (we fix broken code, they address smelly code), though there is significant overlap. We do prefer one question per question, however, and "is this whole thing wrong?" is a little broad for StackOverflow.
This has to be the biggest nuisance I've encountered with PyQT: I've hacked together a thumbnailing thread for my application (I have to thumbnail tons of big images), and it looks like it would work (and it almost does). My main problem is this error message whenever I send a SIGNAL from my thread:
QPixmap: It is not safe to use pixmaps outside the GUI thread
I can't figure out how to get around this. I've tried passing a QIcon through my SIGNAL, but that still generates the same error. If it helps, here's the code blocks which deal with this stuff:
The Thumbnailer class:
class Thumbnailer(QtCore.QThread):
def __init__(self, ListWidget, parent = None):
super(Thumbnailer, self).__init__(parent)
self.stopped = False
self.completed = False
self.widget = ListWidget
def initialize(self, queue):
self.stopped = False
self.completed = False
self.queue = queue
def stop(self):
self.stopped = True
def run(self):
self.process()
self.stop()
def process(self):
for i in range(self.widget.count()):
item = self.widget.item(i)
icon = QtGui.QIcon(str(item.text()))
pixmap = icon.pixmap(72, 72)
icon = QtGui.QIcon(pixmap)
item.setIcon(icon)
The part which calls the thread (it occurs when a set of images is dropped onto the list box):
self.thread.images.append(f)
item = QtGui.QListWidgetItem(f, self.ui.pageList)
item.setStatusTip(f)
self.thread.start()
I'm not sure how to handle this kind of stuff, as I'm just a GUI newbie ;)
Thanks to all.
After many attempts, I finally got it. I can't use a QIcon or QPixmap from within a non-GUI thread, so I had to use a QImage instead, as that transmits fine.
Here's the magic code:
Excerpt from the thumbnailer.py QThread class:
icon = QtGui.QImage(image_file)
self.emit(QtCore.SIGNAL('makeIcon(int, QImage)'), i, icon)
makeIcon() function:
def makeIcon(self, index, image):
item = self.ui.pageList.item(index)
pixmap = QtGui.QPixmap(72, 72)
pixmap.convertFromImage(image) # <-- This is the magic function!
icon = QtGui.QIcon(pixmap)
item.setIcon(icon)
Hope this helps anyone else trying to make an image thumbnailing thread ;)