Related
I'm trying to create a grid of square buttons that is scrollable if the window is too small to show all of them. I'd like there to be labels on the left-most column and top-most row showing the button indices.
Is there a way to create a QScrollArea with the widgets (labels) in the top-most row and left-most column "frozen". Similar to how you can freeze rows and columns in an Excel Sheet where they follow the view around as you scroll.
See a mockup here:
Either Qt and PyQt are welcome.
I solved my problem with multiple QScrollAreas using the method outlined in this answer. The idea is to have the frozen areas be QScrollArea with disabled scrolling, while the unfrozen QScrollArea scrollbar signals are connected to the frozen QScrollArea scrollbar slots.
Here is the code of my mockup with the top-most row and left-most column frozen. The especially relevant parts are the FrozenScrollArea class and the connections inside the Window class.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QPushButton,
QWidget,
QScrollArea,
QGridLayout,
QLabel,
QFrame,
QSpacerItem,
QSizePolicy,
)
ROWS = 10
COLS = 20
SIZE = 35
style = """
Button {
padding: 0;
margin: 0;
border: 1px solid black;
}
Button::checked {
background-color: lightgreen;
}
"""
class Button(QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(SIZE, SIZE)
self.setCheckable(True)
self.setStyleSheet(style)
class Label(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAlignment(Qt.AlignCenter)
self.setFixedSize(SIZE, SIZE)
class Labels(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QGridLayout()
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
class FrozenScrollArea(QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.verticalScrollBar().setEnabled(False)
self.horizontalScrollBar().setEnabled(False)
class FrozenRow(FrozenScrollArea):
def __init__(self, parent):
super().__init__()
labels = Labels(parent)
for c in range(COLS):
label = Label(self, text = str(c))
labels.layout().addWidget(label, 0, c, 1, 1, Qt.AlignCenter)
labels.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, COLS, 1, 1)
self.setFrameShape(QFrame.NoFrame)
self.setFixedHeight(SIZE)
self.setWidget(labels)
class FrozenColumn(FrozenScrollArea):
def __init__(self, parent):
super().__init__()
labels = Labels(parent)
for r in range(ROWS):
label = Label(self, text = str(r))
labels.layout().addWidget(label, r, 0, 1, 1, Qt.AlignCenter)
labels.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), ROWS, 0, 1, 1)
self.setFrameShape(QFrame.NoFrame)
self.setFixedWidth(SIZE)
self.setWidget(labels)
class ButtonGroup(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QGridLayout()
for r in range(ROWS):
for c in range(COLS):
button = Button(self)
layout.addWidget(button, r, c, 1, 1)
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
class Buttons(QScrollArea):
def __init__(self, parent):
super().__init__()
self.setFrameShape(QFrame.NoFrame)
self.setWidget(ButtonGroup(parent))
class Window(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# layout
layout = QGridLayout()
self.setLayout(layout)
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# frozen row (top)
self.frozenRow = FrozenRow(self)
layout.addWidget(self.frozenRow, 0, 1, 1, 1)
# frozen column (left)
self.frozenColumn = FrozenColumn(self)
layout.addWidget(self.frozenColumn, 1, 0, 1, 1)
# button grid
self.buttons = Buttons(self)
layout.addWidget(self.buttons, 1, 1, 1, 1)
# scrollbar connections
self.buttons.horizontalScrollBar().valueChanged.connect(self.frozenRow.horizontalScrollBar().setValue) # horizontal scroll affects frozen row only
self.buttons.verticalScrollBar().valueChanged.connect(self.frozenColumn.verticalScrollBar().setValue) # vertical scroll affects frozemn column only
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec())
While the frozen scroll area method is effective, it has some drawbacks; most importantly, it:
is not dynamic;
does not consider basic box layouts;
does not support different directions (for boxed layouts) or origin points (for grid layouts);
While this is more a "fringe case", I'd like to suggest an alternative, based on QHeaderView and a "private" model that uses the layout manager for the header sizes.
It doesn't directly support resizing as one would expect from a standard QHeaderView, but that's almost impossible: for boxed layouts it's not possible to set a layout item size (if not by completely overriding the way the layout sets geometries), and for grid layouts there's no way to know if rows or columns are "actually" removed, since rowCount() and columnCount() are never updated dynamically when the grid size changes.
The concept is based on overriding the event filter of the scroll area and check whether geometry changes are happening and if the layout has to lay out items again. Then, the implementation uses the layout information to update the underlying model and provide appropriate values for the SizeHintRole for headerData().
The subclassed QScrollArea creates two QHeaderViews and updates them whenever required using the ResizeToContents section resize mode (which queries headerData()) and uses setViewportMargins based on the size hints of the headers.
class LayoutModel(QtCore.QAbstractTableModel):
reverse = {
QtCore.Qt.Horizontal: False,
QtCore.Qt.Vertical: False
}
def __init__(self, rows=None, columns=None):
super().__init__()
self.rows = rows or []
self.columns = columns or []
def setLayoutData(self, hSizes, vSizes, reverseH=False, reverseV=False):
self.beginResetModel()
self.reverse = {
QtCore.Qt.Horizontal: reverseH,
QtCore.Qt.Vertical: reverseV
}
self.rows = vSizes
self.columns = hSizes
opt = QtWidgets.QStyleOptionHeader()
opt.text = str(len(vSizes))
style = QtWidgets.QApplication.style()
self.headerSizeHint = style.sizeFromContents(style.CT_HeaderSection, opt, QtCore.QSize())
self.endResetModel()
def rowCount(self, parent=None):
return len(self.rows)
def columnCount(self, parent=None):
return len(self.columns)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if self.reverse[orientation]:
if orientation == QtCore.Qt.Horizontal:
section = len(self.columns) - 1 - section
else:
section = len(self.rows) - 1 - section
# here you can add support for custom header labels
return str(section + 1)
elif role == QtCore.Qt.SizeHintRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QSize(self.columns[section], self.headerSizeHint.height())
return QtCore.QSize(self.headerSizeHint.width(), self.rows[section])
def data(self, *args, **kwargs):
pass # not really required, but provided for consistency
class ScrollAreaLayoutHeaders(QtWidgets.QScrollArea):
_initialized = False
def __init__(self):
super().__init__()
self.hHeader = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, self)
self.vHeader = QtWidgets.QHeaderView(QtCore.Qt.Vertical, self)
self.layoutModel = LayoutModel()
for header in self.hHeader, self.vHeader:
header.setModel(self.layoutModel)
header.setSectionResizeMode(header.Fixed)
self.updateTimer = QtCore.QTimer(
interval=0, timeout=self.updateHeaderSizes, singleShot=True)
def layout(self):
try:
return self.widget().layout()
except AttributeError:
pass
def eventFilter(self, obj, event):
if obj == self.widget() and obj.layout() is not None:
if event.type() in (event.Resize, event.Move):
if self.sender() in (self.verticalScrollBar(), self.horizontalScrollBar()):
self.updateGeometries()
else:
self.updateHeaderSizes()
elif event.type() == event.LayoutRequest:
self.widget().adjustSize()
self.updateTimer.start()
return super().eventFilter(obj, event)
def updateHeaderSizes(self):
layout = self.layout()
if layout is None:
self.layoutModel.setLayoutData([], [])
self.updateGeometries()
return
self._initialized = True
hSizes = []
vSizes = []
layGeo = self.widget().rect()
reverseH = reverseV = False
if isinstance(layout, QtWidgets.QBoxLayout):
count = layout.count()
direction = layout.direction()
geometries = [layout.itemAt(i).geometry() for i in range(count)]
# LeftToRight and BottomToTop layouts always have a first bit set
reverse = direction & 1
if reverse:
geometries.reverse()
lastPos = 0
lastGeo = geometries[0]
if layout.direction() in (layout.LeftToRight, layout.RightToLeft):
if reverse:
reverseH = True
vSizes.append(layGeo.bottom())
lastExt = lastGeo.x() + lastGeo.width()
for geo in geometries[1:]:
newPos = lastExt + (geo.x() - lastExt) / 2
hSizes.append(newPos - lastPos)
lastPos = newPos
lastExt = geo.x() + geo.width()
hSizes.append(layGeo.right() - lastPos - 1)
else:
if reverse:
reverseV = True
hSizes.append(layGeo.right())
lastExt = lastGeo.y() + lastGeo.height()
for geo in geometries[1:]:
newPos = lastExt + (geo.y() - lastExt) / 2
vSizes.append(newPos - lastPos)
lastPos = newPos
lastExt = geo.y() + geo.height()
vSizes.append(layGeo.bottom() - lastPos + 1)
else:
# assume a grid layout
origin = layout.originCorner()
if origin & 1:
reverseH = True
if origin & 2:
reverseV = True
first = layout.cellRect(0, 0)
lastX = lastY = 0
lastRight = first.x() + first.width()
lastBottom = first.y() + first.height()
for c in range(1, layout.columnCount()):
cr = layout.cellRect(0, c)
newX = lastRight + (cr.x() - lastRight) / 2
hSizes.append(newX - lastX)
lastX = newX
lastRight = cr.x() + cr.width()
hSizes.append(layGeo.right() - lastX)
for r in range(1, layout.rowCount()):
cr = layout.cellRect(r, 0)
newY = lastBottom + (cr.y() - lastBottom) / 2
vSizes.append(newY - lastY)
lastY = newY
lastBottom = cr.y() + cr.height()
vSizes.append(layGeo.bottom() - lastY)
hSizes[0] += 2
vSizes[0] += 2
self.layoutModel.setLayoutData(hSizes, vSizes, reverseH, reverseV)
self.updateGeometries()
def updateGeometries(self):
self.hHeader.resizeSections(self.hHeader.ResizeToContents)
self.vHeader.resizeSections(self.vHeader.ResizeToContents)
left = self.vHeader.sizeHint().width()
top = self.hHeader.sizeHint().height()
self.setViewportMargins(left, top, 0, 0)
vg = self.viewport().geometry()
self.hHeader.setGeometry(vg.x(), 0,
self.viewport().width(), top)
self.vHeader.setGeometry(0, vg.y(),
left, self.viewport().height())
self.hHeader.setOffset(self.horizontalScrollBar().value())
self.vHeader.setOffset(self.verticalScrollBar().value())
def sizeHint(self):
if not self._initialized and self.layout():
self.updateHeaderSizes()
hint = super().sizeHint()
if self.widget():
viewHint = self.viewportSizeHint()
if self.horizontalScrollBarPolicy() == QtCore.Qt.ScrollBarAsNeeded:
if viewHint.width() > hint.width():
hint.setHeight(hint.height() + self.horizontalScrollBar().sizeHint().height())
if self.verticalScrollBarPolicy() == QtCore.Qt.ScrollBarAsNeeded:
if viewHint.height() > hint.height():
hint.setWidth(hint.width() + self.verticalScrollBar().sizeHint().width())
hint += QtCore.QSize(
self.viewportMargins().left(), self.viewportMargins().top())
return hint
def resizeEvent(self, event):
super().resizeEvent(event)
QtCore.QTimer.singleShot(0, self.updateGeometries)
Notes:
the code above will cause some level of recursion; that is expected, as resizing the viewport will obviously trigger a resizeEvent, but Qt is smart enough to ignore them whenever sizes are unchanged;
this will only work for basic QBoxLayouts and QGridLayout; it's untested for QFormLayout and the behavior of other custom QLayout subclasses is completely unexpected;
I am trying to dynamically load a wxGrid with a pandas Dataframe depending on what table is selected in the combobox. I can get the grid to load on intialization, but can't figure out how to get refresh the GridTableClass and grid. Right now I am trying to test with a random Dataframe.
class PageOne(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
#wx.StaticText(self, -1, "This is a PageOne object", (20,20))
class PageTwo(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
#wx.StaticText(self, -1, "This is a PageTwo object", (40, 40))
class PageThree(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
#wx.StaticText(self, -1, "This is a PageThree object", (60, 60))
class DataTable(gridlib.GridTableBase):
def __init__(self, data):
gridlib.GridTableBase.__init__(self)
self.data = data
#self.colnames = colnames
#Store the row and col length to see if table has changed in size
self._rows = self.GetNumberRows()
self._cols = self.GetNumberCols()
self.odd=gridlib.GridCellAttr()
self.odd.SetBackgroundColour((217,217,217))
self.even=gridlib.GridCellAttr()
self.even.SetBackgroundColour((255,255,255))
def GetAttr(self, row, col, kind):
attr = [self.even, self.odd][row % 2]
attr.IncRef()
return attr
def GetNumberRows(self):
return len(self.data)
def GetNumberCols(self):
return len(self.data.columns) + 1
def IsEmptyCell(self, row, col):
return False
def GetValue(self, row, col):
#if col == 0:
# return None #self.data.index[row]
return self.data.iloc[row, col-1]
def SetValue(self, row, col, value):
self.data.iloc[row, col - 1] = value
def GetColLabelValue(self, col):
if col == 0:
return None
#pass
#return 'Index' if self.data.index.name is None else self.data.index.name
return self.data.columns[col - 1] #[col-1]
#return None
#---------------------------------------------------------------------------
class DataGrid(gridlib.Grid):
def __init__(self, parent, data): # data
gridlib.Grid.__init__(self, parent, - 1) #,colnames,-1 # data
#data = pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD'))
#data.reset_index(drop=True, inplace=True)
table = DataTable(data)
print ("passed")
# The second parameter means that the grid is to take ownership of the
# table and will destroy it when done. Otherwise you would need to keep
# a reference to it and call it's Destroy method later.
self.SetTable(table, True)
self.Bind(gridlib.EVT_GRID_CELL_RIGHT_CLICK, self.OnCellRightClick)
def OnCellRightClick(self, event):
print ("OnCellRightClick: (%d,%d)\n" % (event.GetRow(), event.GetCol()))
#-------------------------------------------------------------------------------
class MainFrame(wx.Frame):
def __init__(self, parent, data): # (self, parent, data):
wx.Frame.__init__(self, parent, -1, "Varkey Foundation") #, size=(640,480))
#Create a panel
self.p = wx.Panel(self)
self.Maximize(True)
#Create blank dataframe
data = pd.DataFrame() #pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD')
#data = pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD'))
#data.reset_index(drop=True, inplace=True)
self.data = DataTable(data)
self.nb = wx.Notebook(self.p)
self.p.SetBackgroundColour( wx.Colour( 0, 0, 0 ) ) # 38,38,38
self.nb.SetBackgroundColour(wx.Colour(58, 56, 56) )
#self.SetBackgroundColour( wx.Colour( 255, 255, 56 ) )
#create the page windows as children of the notebook
self.page1 = PageOne(self.nb)
self.page2 = PageTwo(self.nb)
self.page3 = PageThree(self.nb)
# add the pages to the notebook with the label to show on the tab
self.nb.AddPage(self.page1, "Data")
self.nb.AddPage(self.page2, "Analyze")
self.nb.AddPage(self.page3, "Change Log")
#Create the grid and continue layout
self.grid = DataGrid(self.page1, data)
#grid.SetReadOnly(5,5, True)
#CreateFonts
self.b_font = wx.Font(14,wx.ROMAN,wx.NORMAL,wx.BOLD, True)
self.lbl_font = wx.Font(14,wx.ROMAN,wx.NORMAL,wx.NORMAL, True)
self.cb_font = wx.Font(11,wx.SCRIPT,wx.ITALIC,wx.NORMAL, True)
self.h_font = wx.Font(18,wx.DECORATIVE,wx.ITALIC,wx.BOLD, True)
#Create Title bmp
ico = wx.Icon('varkey_bmp.bmp', wx.BITMAP_TYPE_ICO) #'varkey_frame.bmp'
self.SetIcon(ico)
#Page 1 sizers and widgets
self.title = wx.StaticText(self.page1,label="TITLE",style = wx.ALIGN_CENTER | wx.ST_NO_AUTORESIZE)
self.title.SetForegroundColour((255,255,255))
self.title.SetFont(self.h_font)
self.p1_sizer = wx.BoxSizer(wx.VERTICAL)
self.p1_sizer.Add(self.title,0,wx.EXPAND,5)
self.p1_sizer.Add(self.grid,3,wx.EXPAND | wx.ALL ,25)
#self.p1_sizer.Add(self.btn_new,-0,wx.ALIGN_CENTER,5)
self.page1.SetSizer(self.p1_sizer)
#Page 2 sizers and widgets
self.analyze_grid = gridlib.Grid(self.page2)
self.analyze_grid.CreateGrid(0, 10)
self.p2_sizer = wx.BoxSizer(wx.VERTICAL)
self.p2_sizer.Add(self.analyze_grid,1,wx.EXPAND)
self.page2.SetSizer(self.p2_sizer)
#Page 3 sizers and widgets
self.log_grid = gridlib.Grid(self.page3)
self.log_grid.CreateGrid(0, 9)
self.log_grid.EnableEditing(False)
self.p3_sizer = wx.BoxSizer(wx.VERTICAL)
self.p3_sizer.Add(self.log_grid,1,wx.EXPAND)
self.page3.SetSizer(self.p3_sizer)
#Create widgets for top sizer
#Insert Image
self.staticbitmap = wx.StaticBitmap(self.p)
self.staticbitmap.SetBitmap(wx.Bitmap('varkey_logo2.jpg'))
self
self.lbl_user = wx.StaticText(self.p,label="Username:")
self.lbl_password = wx.StaticText(self.p,label="Password:")
self.lbl_interaction = wx.StaticText(self.p,label="Interaction:")
self.lbl_table = wx.StaticText(self.p,label="Table:")
#SetForground colors
self.lbl_user.SetForegroundColour((255,255,255))
self.lbl_password.SetForegroundColour((255,255,255))
self.lbl_interaction.SetForegroundColour((255,255,255))
self.lbl_table.SetForegroundColour((255,255,255))
#Set Fonts
self.lbl_user.SetFont(self.lbl_font)
self.lbl_password.SetFont(self.lbl_font)
self.lbl_interaction.SetFont(self.lbl_font)
self.lbl_table.SetFont(self.lbl_font)
self.tc_user =wx.TextCtrl(self.p,value='cmccall95',size = (130,25))
self.tc_password =wx.TextCtrl(self.p,value='Achilles95', style=wx.TE_PASSWORD | wx.TE_PROCESS_ENTER,size = (130,25))
#self.tc_password.Bind(wx.EVT_TEXT_ENTER,self.onLogin)
self.tc_user.SetFont(self.cb_font)
self.tc_password.SetFont(self.cb_font)
self.btn_login = wx.Button(self.p,label="Login", size=(105,30))
self.btn_login.SetBackgroundColour(wx.Colour(198, 89, 17))
self.btn_login.SetFont(self.b_font)
self.btn_login.Bind(wx.EVT_BUTTON, self.onLogin) #connect_mysql
self.btn_logout = wx.Button(self.p,label="Logout",size=(105,30))
self.btn_logout.SetBackgroundColour(wx.Colour(192,0,0))
self.btn_logout.SetFont(self.b_font)
#self.btn_logout.Bind(wx.EVT_BUTTON, self.onLogout)
self.combo_interaction = wx.ComboBox(self.p, size = (160,25),style = wx.CB_READONLY | wx.CB_SORT | wx.CB_SORT)
#self.combo_interaction.Bind(wx.EVT_COMBOBOX, self.onComboInteraction)
self.combo_table = wx.ComboBox(self.p, size = (160,25),style = wx.CB_READONLY | wx.CB_SORT | wx.CB_SORT)
#self.combo_table.Bind(wx.EVT_COMBOBOX, self.onHideCommands)
self.combo_interaction.SetFont(self.cb_font)
self.combo_table.SetFont(self.cb_font)
#self.combo_table.Bind(wx.EVT_COMBOBOX ,self.OnComboTable)
self.btn_load = wx.Button(self.p,label="Load Table", size=(105,30))
self.btn_load.SetBackgroundColour(wx.Colour(31, 216, 6))
self.btn_load.SetFont(self.b_font)
#self.btn_load.Bind(wx.EVT_BUTTON, self.onLoadData)
self.btn_load.Bind(wx.EVT_BUTTON, self.test_return)
self.lc_change = wx.ListCtrl(self.p,-1,style = wx.TE_MULTILINE | wx.LC_REPORT | wx.LC_VRULES)
self.lc_change.InsertColumn(0,"User ID")
self.lc_change.InsertColumn(1,"Status")
self.lc_change.InsertColumn(2,"Description")
self.lc_change.InsertColumn(3,"Date/Time")
#Set column widths
self.lc_change.SetColumnWidth(0, 75)
self.lc_change.SetColumnWidth(1, 75)
self.lc_change.SetColumnWidth(2, 450)
self.lc_change.SetColumnWidth(3, 125)
#Create Filler text
self.lbl_filler = wx.StaticText(self.p,label="",size = (125,20))
#Create FlexGridSizers(For top half)
self.left_fgs = wx.FlexGridSizer(3,4,25,15)
self.left_fgs.AddMany([(self.lbl_user,1,wx.ALIGN_LEFT | wx.LEFT,15),(self.tc_user,1,wx.EXPAND),(self.lbl_interaction,1,wx.ALIGN_RIGHT|wx.RIGHT, 10),(self.combo_interaction,1,wx.EXPAND),
(self.lbl_password,1,wx.ALIGN_LEFT| wx.LEFT,15),(self.tc_password,1,wx.EXPAND),(self.lbl_table,1,wx.ALIGN_RIGHT|wx.RIGHT, 10),(self.combo_table),
(self.btn_login,2,wx.EXPAND),(self.btn_logout,1,wx.EXPAND),(self.lbl_filler,1,wx.EXPAND),(self.btn_load,1)])
#Create Top Sizer
self.top_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.top_sizer.Add(self.left_fgs,proportion = 1, flag = wx.ALL|wx.EXPAND,border = 30)
self.top_sizer.Add(self.staticbitmap,2,wx.TOP | wx.RIGHT, border = 40) #30
self.top_sizer.Add(self.lc_change,2,wx.RIGHT|wx.EXPAND ,30)
#create Bottom Sizer
self.bottom_sizer = wx.BoxSizer(wx.VERTICAL)
self.bottom_sizer.Add(self.nb,proportion = 5, flag = wx.LEFT |wx.RIGHT | wx.EXPAND,border = 30)
self.mainsizer = wx.BoxSizer(wx.VERTICAL)
self.mainsizer.Add(self.top_sizer,proportion = 0, flag = wx.ALL|wx.EXPAND,border = 5)
self.mainsizer.Add(self.bottom_sizer,proportion = 1,flag = wx.ALL|wx.EXPAND,border = 5)
#self.mainsizer.Add(self.status_sizer,proportion =0,flag = wx.BOTTOM|wx.ALIGN_CENTER_HORIZONTAL, border = 15)
self.p.SetSizerAndFit(self.mainsizer)
def test_reload(self, event):
data = pd.DataFrame() #pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD')
data = pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD'))
#data.reset_index(drop=True, inplace=True)
#self.data = DataTable(data)
self.table.data = self.data
self.grid.Refresh()
#Some more functions.......
if __name__ == '__main__':
import sys
app = wx.App()
frame = MainFrame(None, sys.stdout) # (None, sys.stdout)
frame.Show(True)
app.MainLoop()
I've tried many different variations of the function and can't understand how this should work. From my understanding, I should the table and then call the grid to refresh.
def test_reload(self, event):
data = pd.DataFrame() #pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD')
data = pd.DataFrame(np.random.randint(0,100,size=(200, 5)),columns=list('EFGHD'))
self.table.data = self.data
self.grid.Refresh()
I managed to find a solution. Hopefully this helps someone else out.
To reload the grid, I first create new data. Then I destroy the grid, create a new grid, and insert it in place of the old grid. You must then call Layout(). The Freeze() and Thaw() methods are to prevent screen flickering.
def test_reload(self, event):
self.Freeze()
#Create new data
data = pd.DataFrame(np.random.randint(0,100,size=(40000, 5)),columns=list('EFGHD'))
#Destroy and create grid with new data assigned
self.grid.Destroy()
self.grid = DataGrid(self.page1, data)
#Insert grid into existing sizer
self.p1_sizer.Insert(1,self.grid,1,wx.RIGHT| wx.LEFT|wx.EXPAND, 20)
self.p1_sizer.Layout()
self.Thaw()
I'm sure there is a better method out there, but this works and is instant.
I am sorry for repetitively asking questions regarding pyqt, but the limited resources on it force me to.
I have been trying to implement a resize function for my circle shape, that is an extension of QGraphicsItem, which has an ellipse and a centered text. I am able to resize my shape as desired, but the shape takes a while to catch up to the mouse, i.e on switching directions the circle continues to increase but takes a while to switch directions, moreover the circle resizes with an anchor at the top left corner of the bounding rect.
See this question for a bit of code background
def updateHandlesPos(self):
s = self.handleSize
b = self.boundingRect()
self.handles[self.handleTopLeft] = QRectF(b.left(), b.top(), s, s)
self.handles[self.handleTopMiddle] = QRectF(b.center().x() - s / 2, b.top(), s, s)
self.handles[self.handleTopRight] = QRectF(b.right() - s, b.top(), s, s)
self.handles[self.handleMiddleLeft] = QRectF(b.left(), b.center().y() - s / 2, s, s)
self.handles[self.handleMiddleRight] = QRectF(b.right() - s, b.center().y() - s / 2, s, s)
self.handles[self.handleBottomLeft] = QRectF(b.left(), b.bottom() - s, s, s)
self.handles[self.handleBottomMiddle] = QRectF(b.center().x() - s / 2, b.bottom() - s, s, s)
self.handles[self.handleBottomRight] = QRectF(b.right() - s, b.bottom() - s, s, s)
def interactiveResize(self, mousePos):
self.prepareGeometryChange()
if self.handleSelected in [self.handleTopLeft,
self.handleTopRight,
self.handleBottomLeft,
self.handleBottomRight,
self.handleTopMiddle,
self.handleBottomMiddle,
self.handleMiddleLeft,
self.handleMiddleRight]:
self.radius += (mousePos.y() + mousePos.x() + self.mousePressPos.x() - self.mousePressPos.y())/64
self.setPos(self.x(),self.y())
self.update()
self.updateHandlesPos()
Since the OP has not provided an MRE then I have created an example from scratch. The logic is to track the changes of the items and according to that calculate the new geometry and establish the new position of the other items.
from PyQt5 import QtWidgets, QtGui, QtCore
class GripItem(QtWidgets.QGraphicsPathItem):
circle = QtGui.QPainterPath()
circle.addEllipse(QtCore.QRectF(-5, -5, 10, 10))
square = QtGui.QPainterPath()
square.addRect(QtCore.QRectF(-10, -10, 20, 20))
def __init__(self, annotation_item, index):
super(GripItem, self).__init__()
self.m_annotation_item = annotation_item
self.m_index = index
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setZValue(11)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
def hoverEnterEvent(self, event):
self.setPath(GripItem.square)
self.setBrush(QtGui.QColor("red"))
super(GripItem, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
super(GripItem, self).hoverLeaveEvent(event)
def mouseReleaseEvent(self, event):
self.setSelected(False)
super(GripItem, self).mouseReleaseEvent(event)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
self.m_annotation_item.movePoint(self.m_index, value)
return super(GripItem, self).itemChange(change, value)
class DirectionGripItem(GripItem):
def __init__(self, annotation_item, direction=QtCore.Qt.Horizontal, parent=None):
super(DirectionGripItem, self).__init__(annotation_item, parent)
self._direction = direction
#property
def direction(self):
return self._direction
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
p = QtCore.QPointF(self.pos())
if self.direction == QtCore.Qt.Horizontal:
p.setX(value.x())
elif self.direction == QtCore.Qt.Vertical:
p.setY(value.y())
self.m_annotation_item.movePoint(self.m_index, p)
return p
return super(DirectionGripItem, self).itemChange(change, value)
class CircleAnnotation(QtWidgets.QGraphicsEllipseItem):
def __init__(self, radius=1, parent=None):
super(CircleAnnotation, self).__init__(parent)
self.setZValue(11)
self.m_items = []
self.setPen(QtGui.QPen(QtGui.QColor("green"), 4))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
self._radius = radius
self.update_rect()
#property
def radius(self):
return self._radius
#radius.setter
def radius(self, r):
if r <= 0:
raise ValueError("radius must be positive")
self._radius = r
self.update_rect()
self.add_grip_items()
self.update_items_positions()
def update_rect(self):
rect = QtCore.QRectF(0, 0, 2 * self.radius, 2 * self.radius)
rect.moveCenter(self.rect().center())
self.setRect(rect)
def add_grip_items(self):
if self.scene() and not self.m_items:
for i, (direction) in enumerate(
(
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
)
):
item = DirectionGripItem(self, direction, i)
self.scene().addItem(item)
self.m_items.append(item)
def movePoint(self, i, p):
if 0 <= i < min(4, len(self.m_items)):
item_selected = self.m_items[i]
lp = self.mapFromScene(p)
self._radius = (lp - self.rect().center()).manhattanLength()
k = self.indexOf(lp)
if k is not None:
self.m_items = [item for item in self.m_items if not item.isSelected()]
self.m_items.insert(k, item_selected)
self.update_items_positions([k])
self.update_rect()
def update_items_positions(self, index_no_updates=None):
index_no_updates = index_no_updates or []
for i, (item, direction) in enumerate(
zip(
self.m_items,
(
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
),
),
):
item.m_index = i
if i not in index_no_updates:
pos = self.mapToScene(self.point(i))
item = self.m_items[i]
item._direction = direction
item.setEnabled(False)
item.setPos(pos)
item.setEnabled(True)
def indexOf(self, p):
for i in range(4):
if p == self.point(i):
return i
def point(self, index):
if 0 <= index < 4:
return [
QtCore.QPointF(0, -self.radius),
QtCore.QPointF(self.radius, 0),
QtCore.QPointF(0, self.radius),
QtCore.QPointF(-self.radius, 0),
][index]
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
self.update_items_positions()
return
if change == QtWidgets.QGraphicsItem.ItemSceneHasChanged:
self.add_grip_items()
self.update_items_positions()
return
return super(CircleAnnotation, self).itemChange(change, value)
def hoverEnterEvent(self, event):
self.setBrush(QtGui.QColor(255, 0, 0, 100))
super(CircleAnnotation, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
super(CircleAnnotation, self).hoverLeaveEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(scene)
view.setRenderHints(QtGui.QPainter.Antialiasing)
item = CircleAnnotation()
item.radius = 100
scene.addItem(item)
view.showMaximized()
sys.exit(app.exec_())
I am making a physics simulator using tkinter and I want to have an image on top of a button in dnd, tkinter so that when I move the button, the image moves. How do I do this? The image and code are in the same folder.
I have tried adding an image by using it like a normal button but it gives an error. The code is the code from help(dnd) in python. I am using python 3.6. I need to identify the type of component the button is when I move it
Also, if anyone can tell me how to use collision detect to detect when 2 images are touching.
import tkinter
__all__ = ["dnd_start", "DndHandler"]
def dnd_start(source, event):
h = DndHandler(source, event)
if h.root:
return h
else:
return None
# The class that does the work
class DndHandler:
root = None
def __init__(self, source, event):
if event.num > 5:
return
root = event.widget._root()
try:
root.__dnd
return # Don't start recursive dnd
except AttributeError:
root.__dnd = self
self.root = root
self.source = source
self.target = None
self.initial_button = button = event.num
self.initial_widget = widget = event.widget
self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
self.save_cursor = widget['cursor'] or ""
widget.bind(self.release_pattern, self.on_release)
widget.bind("<Motion>", self.on_motion)
widget['cursor'] = "hand2"
def __del__(self):
root = self.root
self.root = None
if root:
try:
del root.__dnd
except AttributeError:
pass
def on_motion(self, event):
x, y = event.x_root, event.y_root
target_widget = self.initial_widget.winfo_containing(x, y)
source = self.source
new_target = None
while target_widget:
try:
attr = target_widget.dnd_accept
except AttributeError:
pass
else:
new_target = attr(source, event)
if new_target:
break
target_widget = target_widget.master
old_target = self.target
if old_target is new_target:
if old_target:
old_target.dnd_motion(source, event)
else:
if old_target:
self.target = None
old_target.dnd_leave(source, event)
if new_target:
new_target.dnd_enter(source, event)
self.target = new_target
def on_release(self, event):
self.finish(event, 1)
def cancel(self, event=None):
self.finish(event, 0)
def finish(self, event, commit=0):
target = self.target
source = self.source
widget = self.initial_widget
root = self.root
try:
del root.__dnd
self.initial_widget.unbind(self.release_pattern)
self.initial_widget.unbind("<Motion>")
widget['cursor'] = self.save_cursor
self.target = self.source = self.initial_widget = self.root = None
if target:
if commit:
target.dnd_commit(source, event)
else:
target.dnd_leave(source, event)
finally:
source.dnd_end(target, event)
class Icon:
def __init__(self, name):
self.name = name
self.canvas = self.label = self.id = None
def attach(self, canvas, x=10, y=10):
if canvas is self.canvas:
self.canvas.coords(self.id, x, y)
return
if self.canvas:
self.detach()
if not canvas:
return
label = tkinter.Label(canvas, text=self.name,
borderwidth=2, relief="raised")
id = canvas.create_window(x, y, window=label, anchor="nw")
self.canvas = canvas
self.label = label
self.id = id
label.bind("<ButtonPress>", self.press)
def detach(self):
canvas = self.canvas
if not canvas:
return
id = self.id
label = self.label
self.canvas = self.label = self.id = None
canvas.delete(id)
label.destroy()
def press(self, event):
if dnd_start(self, event):
# where the pointer is relative to the label widget:
self.x_off = event.x
self.y_off = event.y
# where the widget is relative to the canvas:
self.x_orig, self.y_orig = self.canvas.coords(self.id)
def move(self, event):
x, y = self.where(self.canvas, event)
self.canvas.coords(self.id, x, y)
def putback(self):
self.canvas.coords(self.id, self.x_orig, self.y_orig)
def where(self, canvas, event):
# where the corner of the canvas is relative to the screen:
x_org = canvas.winfo_rootx()
y_org = canvas.winfo_rooty()
# where the pointer is relative to the canvas widget:
x = event.x_root - x_org
y = event.y_root - y_org
# compensate for initial pointer offset
return x - self.x_off, y - self.y_off
def dnd_end(self, target, event):
pass
class Tester:
def __init__(self, root):
self.top = tkinter.Toplevel(root)
self.canvas = tkinter.Canvas(self.top, width=100, height=100)
self.canvas.pack(fill="both", expand=1)
self.canvas.dnd_accept = self.dnd_accept
def dnd_accept(self, source, event):
return self
def dnd_enter(self, source, event):
self.canvas.focus_set() # Show highlight border
x, y = source.where(self.canvas, event)
x1, y1, x2, y2 = source.canvas.bbox(source.id)
dx, dy = x2-x1, y2-y1
self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
self.dnd_motion(source, event)
def dnd_motion(self, source, event):
x, y = source.where(self.canvas, event)
x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
self.canvas.move(self.dndid, x-x1, y-y1)
def dnd_leave(self, source, event):
self.top.focus_set() # Hide highlight border
self.canvas.delete(self.dndid)
self.dndid = None
def dnd_commit(self, source, event):
self.dnd_leave(source, event)
x, y = source.where(self.canvas, event)
source.attach(self.canvas, x, y)
def test():
root = tkinter.Tk()
root.geometry("+1+1")
tkinter.Button(command=root.quit, text="Quit").pack()
t1 = Tester(root)
t1.top.geometry("+1+60")
t2 = Tester(root)
t2.top.geometry("+120+60")
t3 = Tester(root)
t3.top.geometry("+240+60")
i1 = Icon("ICON1")
i2 = Icon("ICON2")
i3 = Icon("ICON3")
i1.attach(t1.canvas)
i2.attach(t2.canvas)
i3.attach(t3.canvas)
root.mainloop()
if __name__ == '__main__':
test()
I have a program which looks like this:
def aStar(theMap, app):
# Redraws the cell in the GUI
def updateCell(x,y, value):
theMap[y][x] = value
app.gui.drawCell(x,y,value)
# Do a lot of stuff which calls the method above
def solveScenario(scenario):
class App(threading.Thread):
def __init__(self, theMap, n, m, cellSize):
self.root = Tk()
self.root.protocol("WM_DELETE_WINDOW", self.callback)
self.gui = CellGrid(self.root,n,m,cellSize,theMap)
self.gui.pack()
threading.Thread.__init__(self)
self.start()
def callback(self):
self.root.quit()
def run(self):
self.root.mainloop()
prob = Problem(scenario)
app = App(prob.theMap, prob.n, prob.m, 40)
aStar()
app.root.close()
def main():
scenarios = # .. list of scenarios
for i in scenarios:
solveScenario(i)
class CellGrid(Canvas):
theMap = None
def __init__(self,master, rowNumber, columnNumber, cellSize, theMap):
Canvas.__init__(self, master, width = cellSize * columnNumber , height = cellSize * rowNumber)
self.cellSize = cellSize
self.theMap = theMap
#print theMap
self.grid = []
for row in range(rowNumber):
line = []
for column in range(columnNumber):
line.append(Cell(self, column, row, cellSize, theMap[row][column]))
self.grid.append(line)
self.draw()
def draw(self):
for row in self.grid:
for cell in row:
cell.draw()
def drawCell(self, x, y, value):
cell = self.grid[y][x]
cell.value = self.theMap[y][x]
cell.draw()
class Cell():
colors = {
0: 'white', # untried
1: 'black', # obstacle
2: 'green', # start
3: 'red', # finish
4: 'blue', # open
5: 'gray', # closed
6: 'orange', # path
}
def __init__(self, master, x, y, size, value):
self.master = master
self.abs = x
self.ord = y
self.size= size
self.fill = "white"
self.value = value
def setValue(self, value):
self.value = value
def draw(self):
""" order to the cell to draw its representation on the canvas """
if self.master != None :
xmin = self.abs * self.size
xmax = xmin + self.size
ymin = self.ord * self.size
ymax = ymin + self.size
self.master.create_rectangle(xmin, ymin, xmax, ymax, fill=self.colors[self.value], outline = "black")
main()
As you can see, for each scenario(problem), a matrix is drawn using Tkinter which is updated in real time by the A-Star algorithm. This works OK, but when switching from one scenario to another, the map changes. How can I refresh the Tkinter window? Using the code above, the cells stay colored when I generate a new map even though app.root.close() and solveScenario() is called again.
Thanks