Dragging and Copying Images in wxPython - python

I am attempting to modify the DragImage demo from the wxPython examples so that instead of just moving the image around, dragging an image produces a new copy which is draggable, while the original "source" image remains. A decent analogy is that the original images are like a set of widgets to choose from; clicking and dragging on any one of those produces a widget for you to place wherever (and this can be done multiple times) while the source widget remains.
import os
import glob
import wx
import wx.lib.scrolledpanel as scrolled
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent)
frm_pnl = MainPanel(self)
self.Show()
class DragShape:
def __init__(self, bmp):
self.bmp = bmp
self.pos = (0,0)
self.shown = True
self.text = None
self.fullscreen = False
def HitTest(self, pt):
rect = self.GetRect()
return rect.InsideXY(pt.x, pt.y)
def GetRect(self):
#return wx.Rect(self.pos[0], self.pos[1], self.bmp.GetWidth(), self.bmp.GetHeight())
return wx.Rect(self.pos[0], self.pos[1], self.bmp.GetWidth()/2, self.bmp.GetHeight()/2)
def Draw(self, dc, op = wx.COPY):
if self.bmp.Ok():
memDC = wx.MemoryDC()
memDC.SelectObject(self.bmp)
#dc.Blit(self.pos[0], self.pos[1],
# self.bmp.GetWidth(), self.bmp.GetHeight(),
# memDC, 0, 0, op, True)
dc.Blit(self.pos[0], self.pos[1],
self.bmp.GetWidth()/2, self.bmp.GetHeight()/2,
memDC, 0, 0, op, True)
return True
else:
return False
class MainPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1, size = (900, 700))
self.shapes = []
#panel for mechanisms
mechPnl = MechanismPanel(self)
mechSzr = wx.BoxSizer(wx.HORIZONTAL)
mechSzr.Add(mechPnl, 1)
selfSizer = wx.BoxSizer(wx.VERTICAL)
selfSizer.Add(mechSzr, 0, wx.EXPAND)
selfSizer.Layout()
self.SetSizer(selfSizer)
self.dragImage = None
self.dragShape = None
self.hiliteShape = None
self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
#self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_PAINT, self.OnPaint)
mechPnl.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
mechPnl.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
mechPnl.Bind(wx.EVT_MOTION, self.OnMotion)
mechPnl.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
# The mouse is moving
def OnMotion(self, evt):
print "On motion!"
# Ignore mouse movement if we're not dragging.
if not self.dragShape or not evt.Dragging() or not evt.LeftIsDown():
return
# if we have a shape, but haven't started dragging yet
if self.dragShape and not self.dragImage:
# only start the drag after having moved a couple pixels
tolerance = 2
pt = evt.GetPosition()
dx = abs(pt.x - self.dragStartPos.x)
dy = abs(pt.y - self.dragStartPos.y)
if dx <= tolerance and dy <= tolerance:
return
# refresh the area of the window where the shape was so it
# will get erased.
self.dragShape.shown = False
self.RefreshRect(self.dragShape.GetRect(), True)
self.Update()
if self.dragShape.text:
self.dragImage = wx.DragString(self.dragShape.text,
wx.StockCursor(wx.CURSOR_HAND))
else:
self.dragImage = wx.DragImage(self.dragShape.bmp,
wx.StockCursor(wx.CURSOR_HAND))
hotspot = self.dragStartPos - self.dragShape.pos
self.dragImage.BeginDrag(hotspot, self, self.dragShape.fullscreen)
self.dragImage.Move(pt)
self.dragImage.Show()
# if we have shape and image then move it, posibly highlighting another shape.
elif self.dragShape and self.dragImage:
onShape = self.FindShape(evt.GetPosition())
unhiliteOld = False
hiliteNew = False
# figure out what to hilite and what to unhilite
if self.hiliteShape:
if onShape is None or self.hiliteShape is not onShape:
unhiliteOld = True
if onShape and onShape is not self.hiliteShape and onShape.shown:
hiliteNew = True
# if needed, hide the drag image so we can update the window
if unhiliteOld or hiliteNew:
self.dragImage.Hide()
if unhiliteOld:
dc = wx.ClientDC(self)
self.hiliteShape.Draw(dc)
self.hiliteShape = None
if hiliteNew:
dc = wx.ClientDC(self)
self.hiliteShape = onShape
self.hiliteShape.Draw(dc, wx.INVERT)
# now move it and show it again if needed
self.dragImage.Move(evt.GetPosition())
if unhiliteOld or hiliteNew:
self.dragImage.Show()
# Left mouse button up.
def OnLeftUp(self, evt):
print "On left up!"
if not self.dragImage or not self.dragShape:
self.dragImage = None
self.dragShape = None
return
# Hide the image, end dragging, and nuke out the drag image.
self.dragImage.Hide()
self.dragImage.EndDrag()
self.dragImage = None
if self.hiliteShape:
self.RefreshRect(self.hiliteShape.GetRect())
self.hiliteShape = None
# reposition and draw the shape
# Note by jmg 11/28/03
# Here's the original:
#
# self.dragShape.pos = self.dragShape.pos + evt.GetPosition() - self.dragStartPos
#
# So if there are any problems associated with this, use that as
# a starting place in your investigation. I've tried to simulate the
# wx.Point __add__ method here -- it won't work for tuples as we
# have now from the various methods
#
# There must be a better way to do this :-)
#
self.dragShape.pos = (
self.dragShape.pos[0] + evt.GetPosition()[0] - self.dragStartPos[0],
self.dragShape.pos[1] + evt.GetPosition()[1] - self.dragStartPos[1]
)
self.dragShape.shown = True
self.RefreshRect(self.dragShape.GetRect())
self.dragShape = None
# Fired whenever a paint event occurs
def OnPaint(self, evt):
print "On paint!"
dc = wx.PaintDC(self)
self.PrepareDC(dc)
self.DrawShapes(dc)
# Left mouse button is down.
def OnLeftDown(self, evt):
print "On left down!"
# Did the mouse go down on one of our shapes?
shape = self.FindShape(evt.GetPosition())
# If a shape was 'hit', then set that as the shape we're going to
# drag around. Get our start position. Dragging has not yet started.
# That will happen once the mouse moves, OR the mouse is released.
if shape:
self.dragShape = shape
self.dragStartPos = evt.GetPosition()
# Go through our list of shapes and draw them in whatever place they are.
def DrawShapes(self, dc):
for shape in self.shapes:
if shape.shown:
shape.Draw(dc)
# This is actually a sophisticated 'hit test', but in this
# case we're also determining which shape, if any, was 'hit'.
def FindShape(self, pt):
for shape in self.shapes:
if shape.HitTest(pt):
return shape
return None
# Clears the background, then redraws it. If the DC is passed, then
# we only do so in the area so designated. Otherwise, it's the whole thing.
def OnEraseBackground(self, evt):
dc = evt.GetDC()
if not dc:
dc = wx.ClientDC(self)
rect = self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
self.TileBackground(dc)
# tile the background bitmap
def TileBackground(self, dc):
sz = self.GetClientSize()
w = self.bg_bmp.GetWidth()
h = self.bg_bmp.GetHeight()
x = 0
while x < sz.width:
y = 0
while y < sz.height:
dc.DrawBitmap(self.bg_bmp, x, y)
y = y + h
x = x + w
# We're not doing anything here, but you might have reason to.
# for example, if you were dragging something, you might elect to
# 'drop it' when the cursor left the window.
def OnLeaveWindow(self, evt):
pass
class MechanismPanel(scrolled.ScrolledPanel):
def __init__(self, parent):
scrolled.ScrolledPanel.__init__(self, parent, -1, size = (400, 140))
self.SetBackgroundColour((211, 211, 211))
mechPnlSzr = wx.BoxSizer(wx.HORIZONTAL)
os.chdir("./figures")
position = 50
for file in glob.glob("icon*.png"):
print file
imgIcon = wx.Image(file, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
staticBitmap = wx.StaticBitmap(self, -1, imgIcon, (position, 50), (50, 50))
shape = DragShape(staticBitmap.GetBitmap())
shape.pos = (position, 50)
position = position + 100
shape.fullscreen = True
parent.shapes.append(shape)
mechPnlSzr.Add(staticBitmap, 0, wx.FIXED, border = 20)
self.SetSizer(mechPnlSzr)
self.SetAutoLayout(1)
self.SetupScrolling()#scroll_y = False)
app = wx.App(False)
frame = MainWindow(None, "Trading Client")
app.MainLoop()

Related

Tkinter: changing canvas pixel size

Here is what I want to do:
I am designing an application that allows, among other things, to view DICOM images. What I will call "image" is actually a set of files that contain 2D arrays corresponding to slices. I am using Tkinter to provide an UI. My application only requires to display binary images.
In order to display the slices, I use tk.Canvas which allows very fast display of images. Indeed, I need the most optimised displaying device since I want to the user to be able to travel across slices using the mouse wheel.
The problem is: when displaying a slice, the canvas is always allocating the same dimensions to pixels and therefore, images with lower resolution appear very small. What I want to do is to prevent the user from killing his/her eyes by resizing the canvas.
I thought of course of using PIL.Image().resize() on the image that is then converted to PIL.ImageTk() but this causes two problems:
The greater the resizing, the more time is needed to perform the process and therefore the less the viewer is optimised
This resizing action actually modifies the number of pixels, which loses the original resolution. I do not want this to happen since I require to retrieve mouse position as it hovers over the canvas, in terms of pixels in the original resolution
The solution in my opinion would therefore be to modify the pixel size of the canvas. If it is possible to define it from the start, then resizing would not be necessary and there would be no optimisation problem.
But I have not been able to find a way to modify this. Would anyone have an idea?
I am providing only the frame and the imager of my project if it can help:
Frame:
import PIL.Image
import PIL.ImageTk
import numpy as np
from gui.statusbar import *
from tkinter.messagebox import showinfo
class DicomViewerFrame(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.image_ax = None
self.image_sag = None
self.photo_ax = None
self.photo_sag = None
self.canvas_axial = None
self.canvas_sagittal = None
self.imager = None
self.arr_axial = None
self.arr_sagittal = None
self.index_axial = None
self.index_sagittal = None
self.status_axial = None
self.status_sagittal = None
self.roi_definition = True
self.upper = 0
self.lower = 0
self.selection_axial = None
self.selection_sagittal = None
self.start_x = self.start_y = self.start_z = 0
self.end_x = self.end_y = self.end_z = 0
self.roi = None
self.offset = 700
self.init_viewer_frame()
def init_viewer_frame(self):
# Image canvas
self.canvas_axial = Canvas(self, bd=0, highlightthickness=0)
self.canvas_axial.grid(row=0, column=0, sticky="nw")
self.canvas_axial.bind("<MouseWheel>", self.scroll_axial_images)
self.canvas_axial.config(width=self.offset)
if self.roi_definition:
self.canvas_axial.bind("<B1-Motion>", self.on_move_press_axial)
self.canvas_axial.bind("<ButtonPress-1>", self.on_button_press_axial)
self.canvas_axial.bind("<ButtonRelease-1>", self.on_button_release)
self.canvas_sagittal = Canvas(self, bd=0, highlightthickness=0)
self.canvas_sagittal.grid(row=0, column=1, sticky="nw")
self.canvas_sagittal.bind("<MouseWheel>", self.scroll_sagittal_images)
if self.roi_definition:
self.canvas_sagittal.bind("<B1-Motion>", self.on_move_press_sagittal)
self.canvas_sagittal.bind("<ButtonPress-1>", self.on_button_press_sagittal)
self.canvas_sagittal.bind("<ButtonRelease-1>", self.on_button_release)
# Status bar
self.status_axial = StatusBar(self)
self.status_axial.grid(row=3, column=0, sticky="w")
self.status_sagittal = StatusBar(self)
self.status_sagittal.grid(row=3, column=1, sticky="w")
self.canvas_axial.bind('<Motion>', self.motion_axial)
self.canvas_sagittal.bind('<Motion>', self.motion_sagittal)
def on_button_press_axial(self, event):
self.canvas_axial.delete(self.selection_axial)
self.canvas_sagittal.delete(self.selection_sagittal)
# save mouse drag start position
self.start_x = event.x
self.start_y = event.y
self.selection_axial = self.canvas_axial.create_rectangle(self.end_x, self.end_y, 0, 0, outline="green")
self.selection_sagittal = self.canvas_sagittal.create_rectangle(0, self.start_x, self.arr_sagittal.shape[1],
self.end_x, outline="green")
def on_button_press_sagittal(self, event):
self.canvas_sagittal.delete(self.selection_sagittal)
# save mouse drag start position
self.start_z = event.x
self.selection_sagittal = self.canvas_sagittal.create_rectangle(self.start_z, self.start_x, 0,
self.end_x, outline="green")
def on_move_press_axial(self, event):
curX, curY = (event.x, event.y)
self.end_x = curX
self.end_y = curY
self.motion_axial(event)
# expand rectangle as you drag the mouse
self.canvas_axial.coords(self.selection_axial, self.start_x, self.start_y, curX, curY)
self.canvas_sagittal.coords(self.selection_sagittal, 0, self.start_x, self.arr_sagittal.shape[1], curX)
def on_move_press_sagittal(self, event):
curZ = event.x
self.end_z = curZ
self.motion_sagittal(event)
# expand rectangle as you drag the mouse
self.canvas_sagittal.coords(self.selection_sagittal, self.start_z, self.start_x, curZ, self.end_x)
def on_button_release(self, event):
roi_axial = self.canvas_axial.bbox(self.selection_axial)
roi_sagittal = self.canvas_sagittal.bbox(self.selection_sagittal)
self.roi = ((roi_axial[0], roi_axial[1], roi_sagittal[0]), (roi_axial[2], roi_axial[3], roi_sagittal[2]))
def show_image(self, array_axial, index_axial, array_sagittal, index_sagittal):
self.upper = int(self.parent.pcd_preparer.get_current_upper().get())
self.lower = int(self.parent.pcd_preparer.get_current_lower().get())
if array_axial is None:
return
if array_sagittal is None:
return
# Convert numpy array into a PhotoImage and add it to canvas
self.image_ax = PIL.Image.fromarray(array_axial)
self.photo_ax = PIL.ImageTk.PhotoImage(self.image_ax)
self.image_sag = PIL.Image.fromarray(array_sagittal)
self.photo_sag = PIL.ImageTk.PhotoImage(self.image_sag)
self.canvas_axial.delete("IMG")
self.canvas_axial.create_image(0, 0, image=self.photo_ax, anchor=NW, tags="IMG")
self.canvas_axial.create_text(40, 10, fill="green", text="Slice " + str(index_axial), font=10)
self.canvas_axial.create_text(40, 40, fill="green", text="Axial", font=10)
self.canvas_sagittal.delete("IMG")
self.canvas_sagittal.create_image(0, 0, image=self.photo_sag, anchor=NW, tags="IMG")
self.canvas_sagittal.create_text(40, 10, fill="green", text="x = " + str(index_sagittal), font=10)
self.canvas_sagittal.create_text(40, 40, fill="green", text="Sagittal", font=10)
width_ax = self.image_ax.width
height_ax = self.image_ax.height
width_sag = self.image_sag.width
height_sag = self.image_sag.height
self.canvas_axial.configure(width=width_ax, height=height_ax)
self.canvas_sagittal.configure(width=width_sag, height=height_sag)
# We need to at least fit the entire image, but don't shrink if we don't have to
width_ax = max(self.parent.winfo_width(), width_ax)
height_ax = max(self.parent.winfo_height(), height_ax + StatusBar.height)
width_sag = max(self.parent.winfo_width(), width_sag)
height_sag = max(self.parent.winfo_height(), height_sag + StatusBar.height)
# Resize root window and prevent resizing smaller than the image
newsize = "{}x{}".format(width_ax + width_sag, height_ax + StatusBar.height)
self.parent.geometry(newsize)
# self.parent.minsize(width_ax + width_sag, height_ax + height_sag)
if self.selection_axial is not None:
self.selection_axial = self.canvas_axial.create_rectangle(self.start_x, self.start_y, self.end_x,
self.end_y, outline="green")
if self.selection_sagittal is not None:
self.selection_sagittal = self.canvas_sagittal.create_rectangle(self.start_z, self.start_x, self.end_z,
self.end_x, outline="green")
def scroll_sagittal_images(self, e):
self.imager.index_sagittal += int(e.delta / 120)
self.arr_sagittal, self.index_sagittal = self.imager.get_current_sagittal_image(self.upper, self.lower)
self.show_image(self.arr_axial, self.index_axial, self.arr_sagittal, self.index_sagittal)
def scroll_axial_images(self, e):
self.imager.index_axial += int(e.delta / 120)
self.arr_axial, self.index_axial = self.imager.get_current_axial_image(self.upper, self.lower)
self.show_image(self.arr_axial, self.index_axial, self.arr_sagittal, self.index_sagittal)
def change_range(self):
self.arr_axial, self.index_axial = self.imager.get_current_axial_image(self.upper, self.lower)
self.arr_sagittal, self.index_sagittal = self.imager.get_current_sagittal_image(self.upper, self.lower)
self.show_image(self.arr_axial, self.index_axial, self.arr_sagittal, self.index_sagittal)
def set_imager(self, im):
self.imager = im
def motion_axial(self, event):
x, y = event.x, event.y
self.status_axial.set('x = {}, y = {}'.format(x, y))
def motion_sagittal(self, event):
z, y = event.x, event.y
self.status_sagittal.set('y = {}, z = {}'.format(y, z))
Imager:
import numpy as np
class DicomImager:
def __init__(self, datasets):
self.values = None
self.datasets = datasets
self._index_axial = 0
self._index_sagittal = 0
self._window_width = 1
self._window_center = 0
self.size = (int(datasets[0].Rows), int(datasets[0].Columns), len(datasets))
self.spacings = (float(datasets[0].PixelSpacing[0]),
float(datasets[0].PixelSpacing[1]),
float(datasets[0].SliceThickness))
self.axes = (np.arange(0.0, (self.size[0] + 1) * self.spacings[0], self.spacings[0]),
np.arange(0.0, (self.size[2] + 1) * self.spacings[2], self.spacings[2]),
np.arange(0.0, (self.size[1] + 1) * self.spacings[1], self.spacings[1]))
# Load pixel data
self.values = np.zeros(self.size, dtype='int32')
for i, d in enumerate(datasets):
# Also performs rescaling. 'unsafe' since it converts from float64 to int32
np.copyto(self.values[:, :, i], d.pixel_array, 'unsafe')
self.max_value = np.amax(self.values)
self.min_value = np.amin(self.values)
#property
def index_sagittal(self):
return self._index_sagittal
#index_sagittal.setter
def index_sagittal(self, value):
while value < 0:
value += self.size[0]
self._index_sagittal = value % self.size[0]
#property
def index_axial(self):
return self._index_axial
#index_axial.setter
def index_axial(self, value):
while value < 0:
value += self.size[2]
self._index_axial = value % self.size[2]
#property
def window_width(self):
return self._window_width
#window_width.setter
def window_width(self, value):
self._window_width = max(value, 1)
#property
def window_center(self):
return self._window_center
#window_center.setter
def window_center(self, value):
self._window_center = value
def get_sagittal_image(self, index, upper, lower):
# int32 true values (HU or brightness units)
img = self.values[index, :, :]
res1 = np.zeros(img.shape)
res1[img < upper] = 1
res1[img < lower] = 0
# Cast to RGB image so that Tkinter can handle it
res = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
res[:, :, 0] = res[:, :, 1] = res[:, :, 2] = res1 * 255
return res
def get_axial_image(self, index, upper, lower):
# int32 true values (HU or brightness units)
img = self.values[:, :, index]
res1 = np.zeros(img.shape)
res1[img < upper] = 1
res1[img < lower] = 0
# Cast to RGB image so that Tkinter can handle it
res = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
res[:, :, 0] = res[:, :, 1] = res[:, :, 2] = res1 * 255
return res
def get_current_sagittal_image(self, upper, lower):
return self.get_sagittal_image(self._index_sagittal, upper, lower), self._index_sagittal
def get_current_axial_image(self, upper, lower):
return self.get_axial_image(self._index_axial, upper, lower), self._index_axial
The solution in my opinion would therefore be to modify the pixel size of the canvas... But I have not been able to find a way to modify this. Would anyone have an idea?
There is no way to modify the size of a pixel in the canvas. Your only option is to resize the image.

Updating location of circle with mouse movement breaks on fast mouse movement, tkinter-python

I am making a draggable point(a circle). The code works, however, while dragging, if the mouse motion is quick the point stops moving. I have taken help from this code for making this program. I will be using this point later on for other purposes. Here is my full code,
from tkinter import *
import sys,os,string,time
class Point():
def __init__(self,canvas,x,y):
self.canvas = canvas
# It could be that we start dragging a widget
# And release it while its on another
# Hence when we land on a widget we set self.loc to 1
# And when we start dragging it we set self.dragged to 1
self.loc = self.dragged = 0
self.x = x
self.y = y
self.radius = 5
self.point = canvas.create_oval(self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius,fill="green",tag="Point")
canvas.tag_bind("Point","<ButtonPress-1>",self.down)
canvas.tag_bind("Point","<ButtonRelease-1>",self.chkup)
canvas.tag_bind("Point","<Enter>",self.enter)
canvas.tag_bind("Point","<Leave>",self.leave)
def down(self,event):
self.loc = 1
self.dragged = 0
event.widget.bind("<Motion>",self.motion)
canvas.itemconfigure(self.point,fill = "red")
def motion(self,event):
root.config(cursor = "exchange")
cnv = event.widget
cnv.itemconfigure(self.point,fill = "red")
self.x,self.y = cnv.canvasx(event.x), cnv.canvasy(event.y)
got = canvas.coords(self.point,self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius)
def enter(self,event):
canvas.itemconfigure(self.point,fill="blue")
self.loc = 1
if self.dragged == event.time:
self.up(event)
def up(self,event):
event.widget.unbind("<Motion>")
canvas.itemconfigure(self.point,fill="green")
self.canvas.update()
def chkup(self,event):
event.widget.unbind("<Motion>")
root.config(cursor = "")
canvas.itemconfigure(self.point,fill="green")
if self.loc: # is button released in the same widget as pressed
self.up(event)
else:
self.dragged = event.time
def leave(self,event):
self.up(event)
root = Tk()
root.title("Drag and Drop")
canvas = Canvas(root,width = 256, height = 256, borderwidth = 1)
point = Point(canvas,128,128)
canvas.pack()
root.mainloop()
Your problem is that your <Leave> binding can fire if you move the mouse outside of the tiny circle faster than you can process the move. That causes the binding for <Motion> to be disabled.
My recommendation is to a) don't bind on <Leave> to disable the binding, and b) bind on <B1-Motion> so that the binding is active only while the button is pressed.

get real size of QPixmap in Qlabel

Is there some simple way in PyQt5 to get real dimensions of the pixmap displayed in QLabel? I am trying to select part of the image with rubber band. But I can't find a way to limit the rubberband only to pixmap. The QLabel.pixmap().rect() returns dimensions of the whole QLabel not only the pixmap. The problem arises when the pixmap is scaled and there are stripes on the sides of the picture.
The Example image
Example image 2
I posted are quite self explanatory. I don't want the rubberband to be able to move out of the picture to the white stripes.
class ResizableRubberBand(QWidget):
def __init__(self, parent=None):
super(ResizableRubberBand, self).__init__(parent)
self.aspect_ratio = None
self.setWindowFlags(Qt.SubWindow)
self.layout = QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.grip1 = QSizeGrip(self)
self.grip2 = QSizeGrip(self)
self.layout.addWidget(self.grip1, 0, Qt.AlignLeft | Qt.AlignTop)
self.layout.addWidget(self.grip2, 0, Qt.AlignRight | Qt.AlignBottom)
self.rubberband = QRubberBand(QRubberBand.Rectangle, self)
self.rubberband.setStyle(QStyleFactory.create("Fusion"))
self.rubberband.move(0, 0)
self.rubberband.show()
self.show()
class ResizablePixmap(QLabel):
def __init__(self, bytes_image):
QLabel.__init__(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
self.setStyleSheet("background-color:#ffffff;")
self.update_pixmap(bytes_image)
def resizeEvent(self, event):
if event:
x = event.size().width()
y = event.size().height()
else:
x = self.width()
y = self.height()
self.current_pixmap = self._bytes2pixmap(self.bytes_image_edit)
self.setPixmap(self.current_pixmap.scaled(x, y, Qt.KeepAspectRatio))
self.resize(x, y)
def update_pixmap(self, bytes_image):
self.bytes_image_edit = bytes_image
self.current_pixmap = self._bytes2pixmap(bytes_image)
self.setPixmap(self.current_pixmap)
self.resizeEvent(None)
#staticmethod
def _bytes2pixmap(raw_image):
image = QImage()
image.loadFromData(raw_image)
return QPixmap(image)
#staticmethod
def _pixmap2bytes(pixmap):
byte_array = QByteArray()
buffer = QBuffer(byte_array)
buffer.open(QIODevice.WriteOnly)
pixmap.save(buffer, 'PNG')
return byte_array.data()
#property
def image_dims(self):
return self.width(), self.height()
def force_resize(self, qsize):
self.resizeEvent(QResizeEvent(qsize, qsize))
class SelectablePixmap(ResizablePixmap):
def __init__(self, bytes_image):
super().__init__(bytes_image)
self.currentQRubberBand = None
self.move_rubber_band = False
self.rubber_band_offset = None
def cancel_selection(self):
self.currentQRubberBand.hide()
self.currentQRubberBand.deleteLater()
self.currentQRubberBand = None
self.selectionActive.emit(False)
def mousePressEvent(self, eventQMouseEvent):
if not self.currentQRubberBand:
self.currentQRubberBand = ResizableRubberBand(self)
self.selectionActive.emit(True)
if self.currentQRubberBand.geometry().contains(eventQMouseEvent.pos()):
self.move_rubber_band = True
self.rubber_band_offset = (eventQMouseEvent.pos() -
self.currentQRubberBand.pos())
else:
self.originQPoint = eventQMouseEvent.pos()
if self.pixmap().rect().contains(self.originQPoint):
self.currentQRubberBand.setGeometry(QRect(self.originQPoint,
QSize()))
self.currentQRubberBand.show()
def mouseMoveEvent(self, eventQMouseEvent):
if self.move_rubber_band:
pos = eventQMouseEvent.pos() - self.rubber_band_offset
if self.pixmap().rect().contains(pos):
self.currentQRubberBand.move(pos)
else:
rect = QRect(self.originQPoint, eventQMouseEvent.pos())
self.currentQRubberBand.setGeometry(rect.normalized())
def mouseReleaseEvent(self, eventQMouseEvent):
if self.move_rubber_band:
self.move_rubber_band = False
The "easy" answer to your question is that you can get the actual geometry of the QPixmap by moving its QRect. Since you're using center alignment, that's very simple:
pixmap_rect = self.pixmap.rect()
pixmap_rect.moveCenter(self.rect().center())
Unfortunately you can't just use that rectangle with your implementation, mostly because you are not really using a QRubberBand.
The concept of a child rubberband, using size grips for resizing, is clever, but has a lot of limitations.
While QSizeGrips make resizing easier, their behavior can't be easily "restricted": you'll probably end up trying to reimplement resize and resizeEvent (risking recursions), maybe with some tricky and convoluted mouse event checking. Also, you'll never be able to resize that "virtual" rubberband to a size smaller to the sum of the QSizeGrips' sizes, nor to a "negative" selection.
Also, in your code you never resize the actual QRubberBand geometry (but that can be done within the ResizableRubberBand.resizeEvent()).
Finally, even if you haven't implemented the selection resizing after an image resizing, you would have a lot of issues if you did (mostly because of the aforementioned minimum size restrainings).
I think that a better solution is to use a simple QRubberBand and implement its interaction directly from the widget that uses it. This lets you have finer control over it, also allowing complete resize features (not only top left and bottom right corners).
I slightly modified your base class code, as you should avoid any resizing within a resizeEvent() (even if it didn't do anything in your case, since the size argument of resize() was the same) and did unnecessary calls to _bytes2pixmap.
class ResizablePixmap(QLabel):
def __init__(self, bytes_image):
QLabel.__init__(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("background-color: #ffffff;")
self.update_pixmap(bytes_image)
def update_pixmap(self, bytes_image):
self.bytes_image_edit = bytes_image
self.current_pixmap = self._bytes2pixmap(bytes_image)
def scale(self, fromResize=False):
# use a single central method for scaling; there's no need to call it upon
# creation and also resize() won't work anyway in a layout
self.setPixmap(self.current_pixmap.scaled(self.width(), self.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
def resizeEvent(self, event):
super(ResizablePixmap, self).resizeEvent(event)
self.scale(True)
#staticmethod
def _bytes2pixmap(raw_image):
image = QImage()
image.loadFromData(raw_image)
return QPixmap(image)
class SelectablePixmap(ResizablePixmap):
selectionActive = pyqtSignal(bool)
def __init__(self, bytes_image):
super().__init__(bytes_image)
# activate mouse tracking to change cursor on rubberband hover
self.setMouseTracking(True)
self.currentQRubberBand = None
self.rubber_band_offset = None
self.moveDirection = 0
def create_selection(self, pos):
if self.currentQRubberBand:
self.cancel_selection()
self.currentQRubberBand = QRubberBand(QRubberBand.Rectangle, self)
self.currentQRubberBand.setStyle(QStyleFactory.create("Fusion"))
self.currentQRubberBand.setGeometry(pos.x(), pos.y(), 1, 1)
self.currentQRubberBand.show()
self.originQPoint = pos
self.currentQRubberBand.installEventFilter(self)
def cancel_selection(self):
self.currentQRubberBand.hide()
self.currentQRubberBand.deleteLater()
self.currentQRubberBand = None
self.originQPoint = None
self.selectionActive.emit(False)
def scale(self, fromResize=False):
if fromResize and self.currentQRubberBand:
# keep data for rubber resizing, before scaling
oldPixmapRect = self.pixmap().rect()
oldOrigin = self.currentQRubberBand.pos() - self.pixmapRect.topLeft()
super(SelectablePixmap, self).scale()
# assuming that you always align the image in the center, get the current
# pixmap rect and move the rectangle center to the current geometry
self.pixmapRect = self.pixmap().rect()
self.pixmapRect.moveCenter(self.rect().center())
if fromResize and self.currentQRubberBand:
# find the new size ratio based on the previous
xRatio = self.pixmapRect.width() / oldPixmapRect.width()
yRatio = self.pixmapRect.height() / oldPixmapRect.height()
# create a new geometry using 0-rounding for improved accuracy
self.currentQRubberBand.setGeometry(
round(oldOrigin.x() * xRatio, 0) + self.pixmapRect.x(),
round(oldOrigin.y() * yRatio + self.pixmapRect.y(), 0),
round(self.currentQRubberBand.width() * xRatio, 0),
round(self.currentQRubberBand.height() * yRatio, 0))
def updateMargins(self):
# whenever the rubber rectangle geometry changes, create virtual
# rectangles for corners and sides to ease up mouse event checking
rect = self.currentQRubberBand.geometry()
self.rubberTopLeft = QRect(rect.topLeft(), QSize(8, 8))
self.rubberTopRight = QRect(rect.topRight(), QSize(-8, 8)).normalized()
self.rubberBottomRight = QRect(rect.bottomRight(), QSize(-8, -8)).normalized()
self.rubberBottomLeft = QRect(rect.bottomLeft(), QSize(8, -8)).normalized()
self.rubberLeft = QRect(self.rubberTopLeft.bottomLeft(), self.rubberBottomLeft.topRight())
self.rubberTop = QRect(self.rubberTopLeft.topRight(), self.rubberTopRight.bottomLeft())
self.rubberRight = QRect(self.rubberTopRight.bottomLeft(), self.rubberBottomRight.topRight())
self.rubberBottom = QRect(self.rubberBottomLeft.topRight(), self.rubberBottomRight.bottomLeft())
self.rubberInnerRect = QRect(self.rubberTop.bottomLeft(), self.rubberBottom.topRight())
def eventFilter(self, source, event):
if event.type() in (QEvent.Resize, QEvent.Move):
self.updateMargins()
return super(SelectablePixmap, self).eventFilter(source, event)
def mousePressEvent(self, event):
pos = event.pos()
if not self.currentQRubberBand or not pos in self.currentQRubberBand.geometry():
if pos not in self.pixmapRect:
self.originQPoint = None
return
self.create_selection(pos)
elif pos in self.rubberTopLeft:
self.originQPoint = self.currentQRubberBand.geometry().bottomRight()
elif pos in self.rubberTopRight:
self.originQPoint = self.currentQRubberBand.geometry().bottomLeft()
elif pos in self.rubberBottomRight:
self.originQPoint = self.currentQRubberBand.geometry().topLeft()
elif pos in self.rubberBottomLeft:
self.originQPoint = self.currentQRubberBand.geometry().topRight()
elif pos in self.rubberTop:
self.originQPoint = self.currentQRubberBand.geometry().bottomLeft()
self.moveDirection = Qt.Vertical
elif pos in self.rubberBottom:
self.originQPoint = self.currentQRubberBand.geometry().topLeft()
self.moveDirection = Qt.Vertical
elif pos in self.rubberLeft:
self.originQPoint = self.currentQRubberBand.geometry().topRight()
self.moveDirection = Qt.Horizontal
elif pos in self.rubberRight:
self.originQPoint = self.currentQRubberBand.geometry().topLeft()
self.moveDirection = Qt.Horizontal
else:
self.rubber_band_offset = pos - self.currentQRubberBand.pos()
def mouseMoveEvent(self, event):
pos = event.pos()
if event.buttons() == Qt.NoButton and self.currentQRubberBand:
if pos in self.rubberTopLeft or pos in self.rubberBottomRight:
self.setCursor(Qt.SizeFDiagCursor)
elif pos in self.rubberTopRight or pos in self.rubberBottomLeft:
self.setCursor(Qt.SizeBDiagCursor)
elif pos in self.rubberLeft or pos in self.rubberRight:
self.setCursor(Qt.SizeHorCursor)
elif pos in self.rubberTop or pos in self.rubberBottom:
self.setCursor(Qt.SizeVerCursor)
elif pos in self.rubberInnerRect:
self.setCursor(Qt.SizeAllCursor)
else:
self.unsetCursor()
elif event.buttons():
if self.rubber_band_offset:
target = pos - self.rubber_band_offset
rect = QRect(target, self.currentQRubberBand.size())
# limit positioning of the selection to the image rectangle
if rect.x() < self.pixmapRect.x():
rect.moveLeft(self.pixmapRect.x())
elif rect.right() > self.pixmapRect.right():
rect.moveRight(self.pixmapRect.right())
if rect.y() < self.pixmapRect.y():
rect.moveTop(self.pixmapRect.y())
elif rect.bottom() > self.pixmapRect.bottom():
rect.moveBottom(self.pixmapRect.bottom())
self.currentQRubberBand.setGeometry(rect)
elif self.originQPoint:
if self.moveDirection == Qt.Vertical:
# keep the X fixed to the current right, so that only the
# vertical position is changed
pos.setX(self.currentQRubberBand.geometry().right())
else:
# limit the X to the pixmapRect extent
if pos.x() < self.pixmapRect.x():
pos.setX(self.pixmapRect.x())
elif pos.x() > self.pixmapRect.right():
pos.setX(self.pixmapRect.right())
if self.moveDirection == Qt.Horizontal:
# same as before, but for the Y position
pos.setY(self.currentQRubberBand.geometry().bottom())
else:
# limit the Y to the pixmapRect extent
if pos.y() < self.pixmapRect.y():
pos.setY(self.pixmapRect.y())
elif pos.y() > self.pixmapRect.bottom():
pos.setY(self.pixmapRect.bottom())
rect = QRect(self.originQPoint, pos)
self.currentQRubberBand.setGeometry(rect.normalized())
def mouseReleaseEvent(self, event):
self.rubber_band_offset = None
self.originQPoint = None
self.moveDirection = 0
You could store width and height of the image (before you create the pixmap from bytes) into global variable and then use getter to access it from outside of class.

tkinter - problem with a coloring a rectangle in a grid made out of rectangles

I am trying to write a pathfinding algorithm in python. The user is supposed to select a starting point by hovering the mouse of a field and pressing s. The field should now change the color.
However, I can't figure out what is wrong with my code. I am only able to color to color the fields from top left corner to the bottom right corner. In the code, Im printing out the objectID in console, which shows that there is maybe something wrong with the way of how I created the rectangles.
I'm creating the rectangles in the draw_grid method in the Window class and coloring the fields in the select_start_node method.
import tkinter as tk
class Window:
def __init__(self):
self.height = 600
self.width = 600
self.grid_list = {x for x in range(0, 600)}
self.grid = []
self.grid_dict = {}
self.root = tk.Tk()
self.root.geometry("600x600")
self.root.resizable(False, False)
self.canvas = tk.Canvas(self.root, width=self.width,
height=self.height, background="white")
self.canvas.bind("s", self.select_start_node)
self.canvas.bind("<1>", lambda event:
self.canvas.focus_set())
def draw_grid(self):
print(self.grid)
for x in self.grid_list:
if x % 30 == 0:
self.grid.append(x)
else:
pass
print(self.grid)
for x in self.grid:
for y in self.grid:
print(x, y+30)
rec = self.canvas.create_rectangle(x, x, y+30, y+30)
self.canvas.pack()
def select_start_node(self, event):
print(event.x, event.y)
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
item = self.canvas.find_closest(x, y)
p = self.canvas.coords(item)
print(item)
print(p)
self.canvas.create_rectangle(p[0], p[0], p[0]+30, p[0]+30, fill="red")
def main():
node_list = []
cord_list = []
window = Window()
window.draw_grid()
window.root.mainloop()
if __name__ == "__main__":
main()
I don't know the entire design of you game, but suggest that you do things differently with respect to the grid of rectangles. In the code below self.grid is a 2-dimensional list-of-lists and each entry is a Canvas rectangle object. This make selecting and changing one of them relatively each because canvas.find_closest(x, y) will give you the object id of the associated rectangle object directly, which makes changing its fill color trivial.
Because of that, I also changed it so you can just click on one of the rectangles to change it instead of moving the mouse cursor and then pressing a key.
Also note that I also got rid of most those hardcoded numerical constants you were using all over the place, which makes the code more flexible in case you decide to change one of them at a later time.
import tkinter as tk
class Window:
def __init__(self):
self.cell_size = 30
self.height = 600
self.width = 600
self.hz_cells = self.width // self.cell_size # Number of horizontal cells.
self.vt_cells = self.height // self.cell_size # Number of vertical cells.
# Preallocate 2D grid (list-of-lists).
self.grid = [[None for _ in range(self.hz_cells)]
for _ in range(self.vt_cells)]
self.root = tk.Tk()
self.root.geometry("%sx%s" % (self.width, self.height))
self.root.resizable(False, False)
self.canvas = tk.Canvas(self.root, width=self.width,
height=self.height, background="white")
self.canvas.pack()
self.canvas.bind("<1>", self.select_start_node)
# You can still do it this way if you want.
# self.canvas.bind("s", self.select_start_node)
# self.canvas.bind("<1>", lambda event: self.canvas.focus_set())
def draw_grid(self):
""" Fill Canvas with a grid of white rectangles. """
for i in range(self.hz_cells):
x = i * self.cell_size
for j in range(self.vt_cells):
y = j * self.cell_size
self.grid[i][j] = self.canvas.create_rectangle(
x, y, x+self.cell_size, y+self.cell_size, fill="white")
def select_start_node(self, event):
""" Change the color of the rectangle closest to x, y of event. """
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
selected_rect = self.canvas.find_closest(x, y)
if selected_rect:
self.canvas.itemconfigure(selected_rect, fill="red") # Change color.
def main():
node_list = []
cord_list = []
window = Window()
window.draw_grid()
window.root.mainloop()
if __name__ == "__main__":
main()

Creating wxSlider with range on Linux

I'm trying to create a slider with option for range selection using wxSlider in Python. It has an optional range parameter but the problem is:
SL_SELRANGE: Allows the user to select a range on the slider. Windows only.
And I'm using Linux.
I thought I might subclass wxSlider and make it work on Linux, or create a custom widget on my own. The problem is I'm not sure how to go about either option.
Any ideas/pointers/pointing me in the right direction would be appreciated.
I tried something like:
range_slider = wx.Slider(parent, wx.ID_ANY, 0, 0, 100, style=wx.SL_HORIZONTAL | wx.SL_LABELS | wx.SL_SELRANGE)
but the "SL_SELRANGE" does nothing on Linux (should provide two handles, to select range).
I'm aware this question is several years old, but even if it's too late to help you, it might help others, as I was going through the same problem recently.
Problem
Even in Windows, the wx.SL_SELRANGE style does not behave as one would expect, creating two independent "thumbs" or handles, which would allow the user to select a range (see this similar question and the documentation). Instead, what it actually does is draw a static band in the trackbar, which does not interact with the single user-controlled thumb. To my knowledge it is not possible to customize the existing wx.Slider control to have two thumbs, since the control is native to the OS.
Solution
In an app I was building I needed to use a control that does what you wanted, but also could not find any good alternatives online. What I ended up doing is creating my own custom RangeSlider widget, which mimics the behavior and functionality of a regular wx.Slider, but with two thumbs:
Notice however that the RangeSlider class handles all the graphics rendering itself and I made it to mimic the Windows 10 look. Therefore the slider appearance will not match the style of a different OS, but it should still work in Linux or OSX. If necessary you could customize the appearance by changing the colors and shapes (all I do is draw rectangles and polygons).
There are some limitations to the widget, it doesn't currently support styles (no ticks or vertical sliders, for example) or validators, but I did implement the wx.EVT_SLIDER event, so other controls can be notified if the values change (this is what I use to dynamically update the text with the slider values, as the user moves the thumbs).
You can find below the code for a working example (it is also available in this GitHub gist, where I may make improvements over time).
import wx
def fraction_to_value(fraction, min_value, max_value):
return (max_value - min_value) * fraction + min_value
def value_to_fraction(value, min_value, max_value):
return float(value - min_value) / (max_value - min_value)
class SliderThumb:
def __init__(self, parent, value):
self.parent = parent
self.dragged = False
self.mouse_over = False
self.thumb_poly = ((0, 0), (0, 13), (5, 18), (10, 13), (10, 0))
self.thumb_shadow_poly = ((0, 14), (4, 18), (6, 18), (10, 14))
min_coords = [float('Inf'), float('Inf')]
max_coords = [-float('Inf'), -float('Inf')]
for pt in list(self.thumb_poly) + list(self.thumb_shadow_poly):
for i_coord, coord in enumerate(pt):
if coord > max_coords[i_coord]:
max_coords[i_coord] = coord
if coord < min_coords[i_coord]:
min_coords[i_coord] = coord
self.size = (max_coords[0] - min_coords[0],
max_coords[1] - min_coords[1])
self.value = value
self.normal_color = wx.Colour((0, 120, 215))
self.normal_shadow_color = wx.Colour((120, 180, 228))
self.dragged_color = wx.Colour((204, 204, 204))
self.dragged_shadow_color = wx.Colour((222, 222, 222))
self.mouse_over_color = wx.Colour((23, 23, 23))
self.mouse_over_shadow_color = wx.Colour((132, 132, 132))
def GetPosition(self):
min_x = self.GetMin()
max_x = self.GetMax()
parent_size = self.parent.GetSize()
min_value = self.parent.GetMin()
max_value = self.parent.GetMax()
fraction = value_to_fraction(self.value, min_value, max_value)
pos = (fraction_to_value(fraction, min_x, max_x), parent_size[1] / 2 + 1)
return pos
def SetPosition(self, pos):
pos_x = pos[0]
# Limit movement by the position of the other thumb
who_other, other_thumb = self.GetOtherThumb()
other_pos = other_thumb.GetPosition()
if who_other == 'low':
pos_x = max(other_pos[0] + other_thumb.size[0]/2 + self.size[0]/2, pos_x)
else:
pos_x = min(other_pos[0] - other_thumb.size[0]/2 - self.size[0]/2, pos_x)
# Limit movement by slider boundaries
min_x = self.GetMin()
max_x = self.GetMax()
pos_x = min(max(pos_x, min_x), max_x)
fraction = value_to_fraction(pos_x, min_x, max_x)
self.value = fraction_to_value(fraction, self.parent.GetMin(), self.parent.GetMax())
# Post event notifying that position changed
self.PostEvent()
def GetValue(self):
return self.value
def SetValue(self, value):
self.value = value
# Post event notifying that value changed
self.PostEvent()
def PostEvent(self):
event = wx.PyCommandEvent(wx.EVT_SLIDER.typeId, self.parent.GetId())
event.SetEventObject(self.parent)
wx.PostEvent(self.parent.GetEventHandler(), event)
def GetMin(self):
min_x = self.parent.border_width + self.size[0] / 2
return min_x
def GetMax(self):
parent_size = self.parent.GetSize()
max_x = parent_size[0] - self.parent.border_width - self.size[0] / 2
return max_x
def IsMouseOver(self, mouse_pos):
in_hitbox = True
my_pos = self.GetPosition()
for i_coord, mouse_coord in enumerate(mouse_pos):
boundary_low = my_pos[i_coord] - self.size[i_coord] / 2
boundary_high = my_pos[i_coord] + self.size[i_coord] / 2
in_hitbox = in_hitbox and (boundary_low <= mouse_coord <= boundary_high)
return in_hitbox
def GetOtherThumb(self):
if self.parent.thumbs['low'] != self:
return 'low', self.parent.thumbs['low']
else:
return 'high', self.parent.thumbs['high']
def OnPaint(self, dc):
if self.dragged or not self.parent.IsEnabled():
thumb_color = self.dragged_color
thumb_shadow_color = self.dragged_shadow_color
elif self.mouse_over:
thumb_color = self.mouse_over_color
thumb_shadow_color = self.mouse_over_shadow_color
else:
thumb_color = self.normal_color
thumb_shadow_color = self.normal_shadow_color
my_pos = self.GetPosition()
# Draw thumb shadow (or anti-aliasing effect)
dc.SetBrush(wx.Brush(thumb_shadow_color, style=wx.BRUSHSTYLE_SOLID))
dc.SetPen(wx.Pen(thumb_shadow_color, width=1, style=wx.PENSTYLE_SOLID))
dc.DrawPolygon(points=self.thumb_shadow_poly,
xoffset=my_pos[0] - self.size[0]/2,
yoffset=my_pos[1] - self.size[1]/2)
# Draw thumb itself
dc.SetBrush(wx.Brush(thumb_color, style=wx.BRUSHSTYLE_SOLID))
dc.SetPen(wx.Pen(thumb_color, width=1, style=wx.PENSTYLE_SOLID))
dc.DrawPolygon(points=self.thumb_poly,
xoffset=my_pos[0] - self.size[0] / 2,
yoffset=my_pos[1] - self.size[1] / 2)
class RangeSlider(wx.Panel):
def __init__(self, parent, id=wx.ID_ANY, lowValue=None, highValue=None, minValue=0, maxValue=100,
pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.SL_HORIZONTAL, validator=wx.DefaultValidator,
name='rangeSlider'):
if style != wx.SL_HORIZONTAL:
raise NotImplementedError('Styles not implemented')
if validator != wx.DefaultValidator:
raise NotImplementedError('Validator not implemented')
super().__init__(parent=parent, id=id, pos=pos, size=size, name=name)
self.SetMinSize(size=(max(50, size[0]), max(26, size[1])))
if minValue > maxValue:
minValue, maxValue = maxValue, minValue
self.min_value = minValue
self.max_value = maxValue
if lowValue is None:
lowValue = self.min_value
if highValue is None:
highValue = self.max_value
if lowValue > highValue:
lowValue, highValue = highValue, lowValue
lowValue = max(lowValue, self.min_value)
highValue = min(highValue, self.max_value)
self.border_width = 8
self.thumbs = {
'low': SliderThumb(parent=self, value=lowValue),
'high': SliderThumb(parent=self, value=highValue)
}
self.thumb_width = self.thumbs['low'].size[0]
# Aesthetic definitions
self.slider_background_color = wx.Colour((231, 234, 234))
self.slider_outline_color = wx.Colour((214, 214, 214))
self.selected_range_color = wx.Colour((0, 120, 215))
self.selected_range_outline_color = wx.Colour((0, 120, 215))
# Bind events
self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)
self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnMouseLost)
self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_SIZE, self.OnResize)
def Enable(self, enable=True):
super().Enable(enable)
self.Refresh()
def Disable(self):
super().Disable()
self.Refresh()
def SetValueFromMousePosition(self, click_pos):
for thumb in self.thumbs.values():
if thumb.dragged:
thumb.SetPosition(click_pos)
def OnMouseDown(self, evt):
if not self.IsEnabled():
return
click_pos = evt.GetPosition()
for thumb in self.thumbs.values():
if thumb.IsMouseOver(click_pos):
thumb.dragged = True
thumb.mouse_over = False
break
self.SetValueFromMousePosition(click_pos)
self.CaptureMouse()
self.Refresh()
def OnMouseUp(self, evt):
if not self.IsEnabled():
return
self.SetValueFromMousePosition(evt.GetPosition())
for thumb in self.thumbs.values():
thumb.dragged = False
if self.HasCapture():
self.ReleaseMouse()
self.Refresh()
def OnMouseLost(self, evt):
for thumb in self.thumbs.values():
thumb.dragged = False
thumb.mouse_over = False
self.Refresh()
def OnMouseMotion(self, evt):
if not self.IsEnabled():
return
refresh_needed = False
mouse_pos = evt.GetPosition()
if evt.Dragging() and evt.LeftIsDown():
self.SetValueFromMousePosition(mouse_pos)
refresh_needed = True
else:
for thumb in self.thumbs.values():
old_mouse_over = thumb.mouse_over
thumb.mouse_over = thumb.IsMouseOver(mouse_pos)
if old_mouse_over != thumb.mouse_over:
refresh_needed = True
if refresh_needed:
self.Refresh()
def OnMouseEnter(self, evt):
if not self.IsEnabled():
return
mouse_pos = evt.GetPosition()
for thumb in self.thumbs.values():
if thumb.IsMouseOver(mouse_pos):
thumb.mouse_over = True
self.Refresh()
break
def OnMouseLeave(self, evt):
if not self.IsEnabled():
return
for thumb in self.thumbs.values():
thumb.mouse_over = False
self.Refresh()
def OnResize(self, evt):
self.Refresh()
def OnPaint(self, evt):
w, h = self.GetSize()
# BufferedPaintDC should reduce flickering
dc = wx.BufferedPaintDC(self)
background_brush = wx.Brush(self.GetBackgroundColour(), wx.SOLID)
dc.SetBackground(background_brush)
dc.Clear()
# Draw slider
track_height = 12
dc.SetPen(wx.Pen(self.slider_outline_color, width=1, style=wx.PENSTYLE_SOLID))
dc.SetBrush(wx.Brush(self.slider_background_color, style=wx.BRUSHSTYLE_SOLID))
dc.DrawRectangle(self.border_width, h/2 - track_height/2, w - 2 * self.border_width, track_height)
# Draw selected range
if self.IsEnabled():
dc.SetPen(wx.Pen(self.selected_range_outline_color, width=1, style=wx.PENSTYLE_SOLID))
dc.SetBrush(wx.Brush(self.selected_range_color, style=wx.BRUSHSTYLE_SOLID))
else:
dc.SetPen(wx.Pen(self.slider_outline_color, width=1, style=wx.PENSTYLE_SOLID))
dc.SetBrush(wx.Brush(self.slider_outline_color, style=wx.BRUSHSTYLE_SOLID))
low_pos = self.thumbs['low'].GetPosition()[0]
high_pos = self.thumbs['high'].GetPosition()[0]
dc.DrawRectangle(low_pos, h / 2 - track_height / 4, high_pos - low_pos, track_height / 2)
# Draw thumbs
for thumb in self.thumbs.values():
thumb.OnPaint(dc)
evt.Skip()
def OnEraseBackground(self, evt):
# This should reduce flickering
pass
def GetValues(self):
return self.thumbs['low'].value, self.thumbs['high'].value
def SetValues(self, lowValue, highValue):
if lowValue > highValue:
lowValue, highValue = highValue, lowValue
lowValue = max(lowValue, self.min_value)
highValue = min(highValue, self.max_value)
self.thumbs['low'].SetValue(lowValue)
self.thumbs['high'].SetValue(highValue)
self.Refresh()
def GetMax(self):
return self.max_value
def GetMin(self):
return self.min_value
def SetMax(self, maxValue):
if maxValue < self.min_value:
maxValue = self.min_value
_, old_high = self.GetValues()
if old_high > maxValue:
self.thumbs['high'].SetValue(maxValue)
self.max_value = maxValue
self.Refresh()
def SetMin(self, minValue):
if minValue > self.max_value:
minValue = self.max_value
old_low, _ = self.GetValues()
if old_low < minValue:
self.thumbs['low'].SetValue(minValue)
self.min_value = minValue
self.Refresh()
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, 'Range Slider Demo', size=(300, 100))
panel = wx.Panel(self)
b = 6
vbox = wx.BoxSizer(orient=wx.VERTICAL)
vbox.Add(wx.StaticText(parent=panel, label='Custom Range Slider:'), flag=wx.ALIGN_LEFT | wx.ALL, border=b)
self.rangeslider = RangeSlider(parent=panel, lowValue=20, highValue=80, minValue=0, maxValue=100,
size=(300, 26))
self.rangeslider.Bind(wx.EVT_SLIDER, self.rangeslider_changed)
vbox.Add(self.rangeslider, proportion=1, flag=wx.EXPAND | wx.ALL, border=b)
self.rangeslider_static = wx.StaticText(panel)
vbox.Add(self.rangeslider_static, flag=wx.ALIGN_LEFT | wx.ALL, border=b)
vbox.Add(wx.StaticText(parent=panel, label='Regular Slider with wx.SL_SELRANGE style:'),
flag=wx.ALIGN_LEFT | wx.ALL, border=b)
self.slider = wx.Slider(parent=panel, style=wx.SL_SELRANGE)
self.slider.SetSelection(20, 40)
self.slider.Bind(wx.EVT_SLIDER, self.slider_changed)
vbox.Add(self.slider, proportion=1, flag=wx.EXPAND | wx.ALL, border=b)
self.slider_static = wx.StaticText(panel)
vbox.Add(self.slider_static, flag=wx.ALIGN_LEFT | wx.ALL, border=b)
self.button_toggle = wx.Button(parent=panel, label='Disable')
self.button_toggle.Bind(wx.EVT_BUTTON, self.toggle_slider_enable)
vbox.Add(self.button_toggle, flag=wx.ALIGN_CENTER | wx.ALL, border=b)
panel.SetSizerAndFit(vbox)
box = wx.BoxSizer()
box.Add(panel, proportion=1, flag=wx.EXPAND)
self.SetSizerAndFit(box)
def slider_changed(self, evt):
obj = evt.GetEventObject()
val = obj.GetValue()
self.slider_static.SetLabel('Value: {}'.format(val))
def rangeslider_changed(self, evt):
obj = evt.GetEventObject()
lv, hv = obj.GetValues()
self.rangeslider_static.SetLabel('Low value: {:.0f}, High value: {:.0f}'.format(lv, hv))
def toggle_slider_enable(self, evt):
if self.button_toggle.GetLabel() == 'Disable':
self.slider.Enable(False)
self.rangeslider.Enable(False)
self.button_toggle.SetLabel('Enable')
else:
self.slider.Enable(True)
self.rangeslider.Enable(True)
self.button_toggle.SetLabel('Disable')
def main():
app = wx.App()
TestFrame().Show()
app.MainLoop()
if __name__ == "__main__":
main()
You could have two sliders; one that will push the other so it remains lower, and one will remain higher?
I know it isn't the same thing, sorry, but it is an option.
So when ever self.minSlider is moved, you bind wx.EVT_SCROLL with a function that will do something like:
self.minSlider.Bind(wx.EVT_SCROLL, self.respondSliderChange())
def respondSliderChange(self):
if self.minSlider.GetValue() >= self.maxSlider.GetValue():
self.maxSlider.SetValue(self.minSlider.GetValue()+1)
and vice-versa for the maxSlider.
Besides that, you can look into creating a custom widget here.
Something related has been described here.
In a nutshell the idea is to draw a box, and colour part of it to represent your range. From the left where your user left-click, and from the right where your user right-click.
Instead of a box and colouring, you could draw some markers on a line:
--------[-----------]--

Categories