I'm trying to create a custom SpinCtrl with a step increment. It seems like such a simple thing so I was surprised the native SpinCtrl doesn't appear to have this functionality, and Google proves uncommonly useless in this matter as well.
When I try to make a custom one, however, I run into problems. Here's some quick and dirty code
class SpinStepCtrl( wx.SpinCtrl ):
def __init__( self, *args, **kwargs ):
super( SpinStepCtrl, self ).__init__( *args, **kwargs )
self.step = 99
self.Bind( wx.EVT_SPINCTRL, self.OnSpin )
#self.Bind( wx.EVT_SPIN_UP, self.OnUp )
#self.Bind( wx.EVT_SPIN_DOWN, self.OnDown )
def OnSpin( self, event ):
print 'X'
self.SetValue( self.GetValue() + self.step )
The print is just there so I can see what, if anything, happens. The EVT_SPIN_UP and EVT_SPIN_DOWN events don't seem to work at all, at least the callbacks are never called which is why I took them out.
When using EVT_SPINCTRL, the callback is called, but end up in an infinite loop because SetValue apparently causes a new such event to be called. It doesn't help much either way, because I can't find a way of telling whether it was a spin up or spin down event, so I can't change the value appropriately.
How do I get this to work?
wx.lib.agw has floatspin widget which has more options. I use it with SetDigits(0) for integer input as well.
import wx
import wx.lib.agw.floatspin as FS
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "FloatSpin Demo")
panel = wx.Panel(self)
floatspin = FS.FloatSpin(panel, -1, pos=(50, 50), min_val=0, max_val=1000,
increment=99, value=0, agwStyle=FS.FS_LEFT)
floatspin.SetDigits(0)
# our normal wxApp-derived class, as usual
app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()
OK, not the best, but works on Ubuntu:
#!/usr/bin/python
import wx
class SpinStepCtrl(wx.SpinCtrl):
def __init__(self, *args, **kwargs):
wx.SpinCtrl.__init__(self, *args, **kwargs)
self.step = 99
self.last_value = 0
self.Bind(wx.EVT_SPINCTRL, self.OnSpin)
def OnSpin(self, event):
delta = self.GetValue() - self.last_value
if delta == 0:
return
elif delta > 0:
self.last_value = self.GetValue() + self.step
else:
self.last_value = self.GetValue() - self.step
self.SetValue(self.last_value)
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self)
self.spin = SpinStepCtrl(self.panel, min=0, max=1000)
self.Show()
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()
Still, I would consider "self.SetValue" generating "wx.EVT_SPINCTRL" a bug.
This works for me:
import wx
class SpinStepCtrl(wx.SpinCtrl):
def __init__(self, *args, **kwargs):
wx.SpinCtrl.__init__(self, *args, **kwargs)
self.step = 99
self.Bind(wx.EVT_SPIN_UP, self.OnUp)
self.Bind(wx.EVT_SPIN_DOWN, self.OnDown)
def OnUp(self, event):
self.SetValue(self.GetValue() + self.step)
def OnDown(self, event):
self.SetValue(self.GetValue() - self.step)
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self)
self.spin = SpinStepCtrl(self.panel, min=0, max=1000)
self.Show()
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()
Here is my solution for wxSpinCtrl, which takes values from a list to spin.
It is a pity that the events EVT_SPIN_UP and EVT_SPIN_DOWN from wx.SpinButton do not work under all platforms (not working here with wx 4.0.6 and Windows 10 / 64 Bit)
import wx
class SpinCtrlList(wx.SpinCtrl):
"""A SpinCtrl that spins through a given list. This list must be in
ascending order and the values of the list must be integers,
whereat negative numbers are allowed
"""
def __init__(self, *args, **kwargs):
wx.SpinCtrl.__init__(self, *args, **kwargs)
self.Bind(wx.EVT_SPINCTRL, self._on_spin)
self.thelist = ([0,1])
self.SetList([0, 1])
self.SetValue(self.thelist[0])
def SetList(self, thelist):
self.thelist = thelist
self.SetRange(self.thelist[0], self.thelist[len(self.thelist)-1])
def GetList(self):
return self.thelist
def SetValue(self, val):
self.sp_old_value = val
super(SpinCtrlList, self).SetValue(val)
def _next_greater_ele(self, in_list, in_val):
for x, val in enumerate(in_list):
if val > in_val:
break
return val
def _next_smaller_ele(self, in_list, in_val):
for x, val in enumerate(reversed(in_list)):
if val < in_val:
break
return val
def _on_spin(self, event):
self.sp_new_value = self.GetValue()
#print(self.sp_old_value, self.sp_new_value)
if self.sp_new_value > self.sp_old_value:
set_val = self._next_greater_ele(self.thelist, self.sp_new_value)
self.SetValue(set_val)
self.sp_old_value = set_val
elif self.sp_new_value < self.sp_old_value:
set_val = self._next_smaller_ele(self.thelist, self.sp_new_value)
self.SetValue(set_val)
self.sp_old_value = set_val
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self)
self.spin = SpinCtrlList(self.panel)
ctrl_list =[100, 200, 500, 1000, 2000, 5000, 10000, 12000, 15000]
#ctrl_list = [-300, -100, 0]
self.spin.SetList(ctrl_list)
self.spin.SetValue(ctrl_list[-2])
self.Show()
def main():
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()
if __name__ == '__main__':
main()
Related
I am making a small application and I have trouble defining efficient "Edit" menu functions.
I have tried this:
from pyautogui import hotkey
.
.
.
def OnCopy ( self, event ):
hotkey ( 'ctrl, 'c' )
However, the above doesn't always work and even breaks sometimes. Is there a better method?
wxPython has its own Clipboard object. Its implementation depends on the use of a wxDataObject because, of course, you can copy and paste many types of data onto the clipboard
import wx
class MainFrame(wx.Frame):
"""Create MainFrame class."""
def __init__(self, *args, **kwargs):
super().__init__(None, *args, **kwargs)
self.size = (400, 1000)
self.panel = MainPanel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.panel)
self.SetSizer(sizer)
self.Center()
self.Show()
def on_copy(self, event):
if wx.TheClipboard.Open():
# Put some text onto the clipboard
text = self.panel.txt_input.GetValue()
data_object = wx.TextDataObject(text)
wx.TheClipboard.SetData(data_object)
# Now retrieve it from the clipboard and print the value
text_data = wx.TextDataObject()
success = wx.TheClipboard.GetData(text_data)
wx.TheClipboard.Close()
if success:
print(f'This data is on the clipboard: {text_data.GetText()}')
class MainPanel(wx.Panel):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.parent = parent
self.txt_input = wx.TextCtrl(self)
cmd_copy = wx.Button(self, wx.ID_COPY)
cmd_copy.Bind(wx.EVT_BUTTON, parent.on_copy)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.txt_input)
sizer.Add(cmd_copy)
self.SetSizer(sizer)
if __name__ == '__main__':
wx_app = wx.App()
MainFrame()
wx_app.MainLoop()
Is there any way I can make certain text on QTextEdit permanent. Applications like cmd.exe where the current user directory is displayed and the rest of the screen is up for input. I tried inserting a QLabel but unable to do so, here's my code, I am currently taking user input through a separate line edit.
UPDATE I had a look at Ipython QtConsole where the line number is displayed constantly, how can I do that, I am looking in the source but if anyone who already knows it, please do tell. Here is the QtConsole for ipython notebook, I am trying to replicate this.
import os
import sys
import PyQt4
import PyQt4.QtCore
from PyQt4.QtGui import *
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
# create objects
label = QLabel(self.tr("Enter command and press Return"))
self.le = QLineEdit()
self.te = QTextEdit()
self.lbl = QLabel(str(os.getcwd())+"> ")
# layout
layout = QVBoxLayout(self)
layout.addWidget(label)
layout.addWidget(self.le)
layout.addWidget(self.te)
self.setLayout(layout)
# styling
self.te.setReadOnly(True)
# create connection
self.mytext = str(self.le.text())
self.connect(self.le, PyQt4.QtCore.SIGNAL("returnPressed(void)"),
self.display)
def display(self):
mytext = str(self.le.text())
self.te.append(self.lbl +str(os.popen(mytext).read()))
self.le.setText("")
if __name__ == "__main__":
main()
A simple solution is to create a class that inherits from QTextEdit and overwrite and add the necessary attributes as shown below:
class TextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.staticText = os.getcwd()
self.counter = 1
self.setReadOnly(True)
def append(self, text):
n_text = "{text} [{number}] > ".format(text=self.staticText, number=self.counter)
self.counter += 1
QTextEdit.append(self, n_text+text)
Complete Code:
import os
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class TextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.staticText = os.getcwd()
self.counter = 1
self.setReadOnly(True)
def append(self, text):
n_text = "{text} [{number}] > ".format(text=self.staticText, number=self.counter)
self.counter += 1
QTextEdit.append(self, n_text+text)
class MyWindow(QWidget):
def __init__(self, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
label = QLabel(self.tr("Enter command and press Return"), self)
self.le = QLineEdit(self)
self.te = TextEdit(self)
# layout
layout = QVBoxLayout(self)
layout.addWidget(label)
layout.addWidget(self.le)
layout.addWidget(self.te)
self.setLayout(layout)
self.connect(self.le, SIGNAL("returnPressed(void)"), self.display)
# self.le.returnPressed.connect(self.display)
def display(self):
command = str(self.le.text())
resp = str(os.popen(command).read())
self.te.append(resp)
self.le.clear()
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
To emulate QtConsole we must overwrite some methods of QTextEdit, catch some events, and verify that it does not eliminate the prefix as I show below:
import os
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class TextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.staticText = os.getcwd()
self.counter = 1
self.prefix = ""
self.callPrefix()
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.onCustomContextMenuRequest)
def onCustomContextMenuRequest(self, point):
menu = self.createStandardContextMenu()
for action in menu.actions():
if "Delete" in action.text():
action.triggered.disconnect()
menu.removeAction(action)
elif "Cu&t" in action.text():
action.triggered.disconnect()
menu.removeAction(action)
elif "Paste" in action.text():
action.triggered.disconnect()
act = menu.exec_(point)
if act:
if "Paste" in act.text():
self.customPaste()
def customPaste(self):
self.moveCursor(QTextCursor.End)
self.insertPlainText(QApplication.clipboard().text())
self.moveCursor(QTextCursor.End)
def clearCurrentLine(self):
cs = self.textCursor()
cs.movePosition(QTextCursor.StartOfLine)
cs.movePosition(QTextCursor.EndOfLine)
cs.select(QTextCursor.LineUnderCursor)
text = cs.removeSelectedText()
def isPrefix(self, text):
return self.prefix == text
def getCurrentLine(self):
cs = self.textCursor()
cs.movePosition(QTextCursor.StartOfLine)
cs.movePosition(QTextCursor.EndOfLine)
cs.select(QTextCursor.LineUnderCursor)
text = cs.selectedText()
return text
def keyPressEvent(self, event):
if event.key() == Qt.Key_Return:
command = self.getCurrentLine()[len(self.prefix):]
self.execute(command)
self.callPrefix()
return
elif event.key() == Qt.Key_Backspace:
if self.prefix == self.getCurrentLine():
return
elif event.matches(QKeySequence.Delete):
return
if event.matches(QKeySequence.Paste):
self.customPaste()
return
elif self.textCursor().hasSelection():
t = self.toPlainText()
self.textCursor().clearSelection()
QTextEdit.keyPressEvent(self, event)
self.setPlainText(t)
self.moveCursor(QTextCursor.End)
return
QTextEdit.keyPressEvent(self, event)
def callPrefix(self):
self.prefix = "{text} [{number}] >".format(text=self.staticText, number=self.counter)
self.counter += 1
self.append(self.prefix)
def execute(self, command):
resp = os.popen(command).read()
self.append(resp)
class MyWindow(QWidget):
def __init__(self, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
label = QLabel(self.tr("Enter command and press Return"), self)
self.te = TextEdit(self)
# layout
layout = QVBoxLayout(self)
layout.addWidget(label)
layout.addWidget(self.te)
self.setLayout(layout)
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
How can I subclass the QPushbutton in Pyside but require an arg. In my example I need the arg to be a list of ints [0,0,0].
My goal is to make it so i can create MyButton like this:
# GOAL
MyButton([0,255,0])
When the arguement containing the list of values is passed in, it should set the value self._data. I'm not sure if i have this setup correctly, so any corrections are appreciated.
class MyButton(QtGui.QPushButton):
def __init__(self, *args, **kwargs):
super(MyButton, self).__init__(*args, **kwargs)
self._data = stuff
#property
def data(self):
return self._data
#data.setter
def data(self, value):
self._data = value
MyButton([0,255,0])
Updated: I noticed though when i pass that value into my Init it doesn't appear to trigger the setter for that property?? Why is that? If you test the code below, you'll see the color of the button isn't set when it's instantiated. How do i fix that?
import os
import sys
import json
from PySide import QtCore, QtGui
class QColorSwatch(QtGui.QPushButton):
colorClicked = QtCore.Signal(list)
colorChanged = QtCore.Signal(list)
def __init__(self, stuff, *args, **kwargs):
super(QColorSwatch, self).__init__(*args, **kwargs)
self._color = stuff
self.setMaximumWidth(22)
self.setMaximumHeight(22)
self.setAutoFillBackground(True)
self.pressed.connect(self.color_clicked)
#property
def color(self):
return self._color
#color.setter
def color(self, value):
self._color = value
pixmap = QtGui.QPixmap(self.size())
pixmap.fill(QtGui.QColor(value[0], value[1], value[2]))
self.setIcon(pixmap)
self.colorChanged.emit(value)
# swatch.setIconSize(pixmap.size() - QtCore.QSize(6,6))
def color_clicked(self):
self.colorClicked.emit(self.color)
class ExampleWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(ExampleWindow, self).__init__(parent)
self.resize(300, 200)
self.ui_swatch = QColorSwatch([255,0,0])
# main layout
main_layout = QtGui.QVBoxLayout()
main_layout.setContentsMargins(5,5,5,5)
main_layout.setSpacing(5)
main_layout.addWidget(self.ui_swatch)
main_widget = QtGui.QWidget()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# Signals
self.ui_swatch.colorClicked.connect(self.color_clicked)
self.ui_swatch.colorChanged.connect(self.color_changed)
def color_clicked(self, col):
print 'CLICKED:', col
self.ui_swatch.color = [255,0,0]
def color_changed(self, col):
print 'CHANGED:', col
def main():
app = QtGui.QApplication(sys.argv)
ex = ExampleWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You have to put stuff as a parameter.
class MyButton(QtGui.QPushButton):
def __init__(self, stuff, *args, **kwargs):
super(MyButton, self).__init__(*args, **kwargs)
self._data = stuff
#property
def data(self):
return self._data
#data.setter
def data(self, value):
self._data = value
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
mainWin = MyButton([0,255,0])
print(mainWin.data)
mainWin.show()
sys.exit(app.exec_())
Update:
You have to use self.color to use the setter.
class QColorSwatch(QtGui.QPushButton):
colorClicked = QtCore.Signal(list)
colorChanged = QtCore.Signal(list)
def __init__(self, stuff, *args, **kwargs):
super(QColorSwatch, self).__init__(*args, **kwargs)
self._color = None
self.color = stuff
self.setMaximumWidth(22)
self.setMaximumHeight(22)
self.setAutoFillBackground(True)
self.pressed.connect(self.color_clicked)
#property
def color(self):
return self._color
#color.setter
def color(self, value):
self._color = value
pixmap = QtGui.QPixmap(self.size())
pixmap.fill(QtGui.QColor(value[0], value[1], value[2]))
self.setIcon(pixmap)
self.colorChanged.emit(value)
# swatch.setIconSize(pixmap.size() - QtCore.QSize(6,6))
def color_clicked(self):
self.colorClicked.emit(self.color)
This is a follow up from allocating more size in sizer to wx.CollapsiblePane when expanded.
EDIT: The answer to that question solved my original problem, which was that nothing moved when I expanded or collapsed a pane, but now I've encountered another bug. While things do move properly, the buttons seem to smear out over each other as shown in the image below. Mousing over a button seems to force it to redraw properly over the rest of the mess, but it leaves a bunch of random button pieces drawn behind and around it.
I've done my absolute best to replicate this bug in a sample app, but I can't. I'm just looking for leads here. Does anyone have any idea what might cause the kind of issue shown below? It only happens after expanding and collapsing some of the upper panes.
For what it's worth, code below is for a sample app that looks very similar, but for some reason doesn't cause the same problems. EDIT Also for what it's worth, here's a link to the full source code for the GUI of my project which generated the screenshot below. http://code.google.com/p/dicom-sr-qi/source/browse/gui/main.py?r=8a876f7b4a034df9747a2c1f2791258f671e44b1
import wx
class SampleSubPanel(wx.Panel):
def __init__(self, *args, **kwargs):
wx.Panel.__init__(self, *args, **kwargs)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(wx.StaticText(self, 1, "A label"))
sizer.Add(wx.SpinCtrl(self))
self.SetSizer(sizer)
class SampleCollapsiblePane(wx.CollapsiblePane):
def __init__(self, *args, **kwargs):
wx.CollapsiblePane.__init__(self,*args,**kwargs)
sizer = wx.BoxSizer(wx.VERTICAL)
for x in range(2):
sizer.Add(wx.CheckBox(self.GetPane(), label = str(x)))
sizer.Add(SampleSubPanel(self.GetPane()))
self.GetPane().SetSizer(sizer)
self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_change)
def on_change(self, event):
self.GetParent().Layout()
class SampleSubPanel2(wx.Panel):
def __init__(self, *args, **kwargs):
wx.Panel.__init__(self, *args, **kwargs)
sizer = wx.BoxSizer(wx.VERTICAL)
for x in range(2):
sizer.Add(SampleCollapsiblePane(self, label = str(x)), 0)
self.SetSizer(sizer)
class Main_Frame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.main_panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(wx.Panel(self.main_panel, style=wx.RAISED_BORDER),1, wx.EXPAND)
sizer.Add(SampleSubPanel2(self.main_panel, style = wx.RAISED_BORDER),2, wx.EXPAND )
sizer.Add(wx.Button(self.main_panel,label= "a button"),0,wx.ALIGN_CENTER)
self.main_panel.SetSizer(sizer)
class SampleApp(wx.App):
def OnInit(self):
frame = Main_Frame(None, title = "Sample App")
frame.Show(True)
frame.Centre()
return True
def main():
app = SampleApp(0)
app.MainLoop()
if __name__ == "__main__":
main()
If I am getting you right, you want the code here - allocating more size in sizer to wx.CollapsiblePane when expanded to work properly. The reason it did not work was you forgot to bind the wx.EVT_COLLAPSIBLEPANE_CHANGED. Here is a code which worked for me -
import wx
class SampleCollapsiblePane(wx.CollapsiblePane):
def __init__(self, *args, **kwargs):
wx.CollapsiblePane.__init__(self,*args,**kwargs)
sizer = wx.BoxSizer(wx.VERTICAL)
for x in range(5):
sizer.Add(wx.Button(self.GetPane(), label = str(x)))
self.GetPane().SetSizer(sizer)
self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_change)
def on_change(self, event):
self.GetParent().Layout()
class Main_Frame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.main_panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
for x in range(5):
sizer.Add(SampleCollapsiblePane(self.main_panel, label = str(x)), 0)
self.main_panel.SetSizer(sizer)
class SampleApp(wx.App):
def OnInit(self):
frame = Main_Frame(None, title = "Sample App")
frame.Show(True)
frame.Centre()
return True
def main():
app = SampleApp(0)
app.MainLoop()
if __name__ == "__main__":
main()
EDIT: Looks like it may be a bug in wxPython running on windows. Below is screen shot of the the exact same code that has problems on Windows running on Ubuntu with no problems.
I didn't find a better way to change the different choices in a wx.ComboBox() than swap the old ComboBox with a new one. Is there a better way?
Oerjan Pettersen
#!/usr/bin/python
#20_combobox.py
import wx
import wx.lib.inspection
class MyFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.p1 = wx.Panel(self)
lst = ['1','2','3']
self.st = wx.ComboBox(self.p1, -1, choices = lst, style=wx.TE_PROCESS_ENTER)
self.st.Bind(wx.EVT_COMBOBOX, self.text_return)
def text_return(self, event):
lst = ['3','4']
self.st = wx.ComboBox(self.p1, -1, choices = lst, style=wx.TE_PROCESS_ENTER)
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, '20_combobox.py')
frame.Show()
self.SetTopWindow(frame)
return 1
if __name__ == "__main__":
app = MyApp(0)
# wx.lib.inspection.InspectionTool().Show()
app.MainLoop()
wx.ComboBox derives from wx.ItemContainer, which has methods for Appending, Clearing, Inserting and Deleting items, all of these methods are available on wx.ComboBox.
One way to do what you want would be to define the text_return() method as follows:
def text_return(self, event):
self.st.Clear()
self.st.Append('3')
self.st.Append('4')