Own drag icon with same color and font settings as the default drag icon in a Gtk.TreeView - pygobject

The Gtk.TreeView implements a default drag icon. It use the background color of the TreeView, it's font and the complete row-content as string.
I want the same (background-color, font-face, font-size, font-color) but with a shorter string (only the second of three columns).
In the example below create my own cairo.Surface to create such an icon. But color and font is a problem. I don't know how to set them up or (much more important) to ask the TreeView or Gtk itself for the current color and font values.
How does the TreeView get this values?
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
import cairo
class MainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="TreeView Drag and Drop")
self.connect("delete-event", Gtk.main_quit)
self.box = Gtk.Box()
self.add(self.box)
# "model" with dummy data
self.store = Gtk.TreeStore(int, str, int)
for i in range(5):
self.store.append(None, [i, 'Item {}'.format(i), i]) # treeview
self.tree = Gtk.TreeView(model=self.store)
self.box.pack_start(self.tree, True, True, 0)
# build columns
colA = Gtk.TreeViewColumn('Col A', Gtk.CellRendererText(), text=0)
self.tree.append_column(colA)
colB = Gtk.TreeViewColumn('Col B', Gtk.CellRendererText(), text=1)
self.tree.append_column(colB)
colC = Gtk.TreeViewColumn('Col C', Gtk.CellRendererText(), text=2)
self.tree.append_column(colC)
# enable default drag and drop
self.tree.set_reorderable(True)
# DnD events
self.tree.connect_after("drag-begin", self.drag_begin)
def drag_begin(self, widget, context):
model, path = widget.get_selection().get_selected_rows()
text = model[path][1]
# dummy surface/context
surface = cairo.ImageSurface(cairo.Format.RGB24, 0, 0)
cr = cairo.Context(surface)
# calculate text size
txtext = cr.text_extents(text)
width = int(txtext.width)
height = int(txtext.height)
offset = 10
# creal surface/context
surface = cairo.ImageSurface(cairo.Format.RGB24,
width + (offset*2),
height + (offset*2))
cr = cairo.Context(surface)
cr.set_source_rgb(1, 1, 1) # text color: white
cr.move_to(0+offset, height+offset)
cr.show_text(text)
# use the surface as drag icon
Gtk.drag_set_icon_surface(context, surface)
win = MainWindow()
win.show_all()
Gtk.main()
What I tried (but not worked) was cairo.Surface.create_similar()',cairo.Surface.create_similar_image()andGtk.TreeView.create_row_drag_icon()`.

This answer is based on a foreign mailing list posting.
The widget has a Gtk.StyleContext. A Pango.Layout is used to render the text based on the style informations in the Gtk.StyleContext.
def drag_begin(self, widget, context):
model, path = widget.get_selection().get_selected_rows()
text = model[path][1]
stylecontext = widget.get_style_context()
# new pango layout
pl = widget.create_pango_layout(text)
ink_rec, log_rect = pl.get_pixel_extents()
padding = 5
# create surface/context
surface = cairo.ImageSurface(cairo.Format.RGB24,
log_rect.width + (padding*2),
log_rect.height + (padding*2))
cr = cairo.Context(surface)
Gtk.render_background(stylecontext, cr, 0, 0,
log_rect.width + (padding*2),
log_rect.height + (padding*2))
Gtk.render_layout(stylecontext, cr, padding, padding, pl)
# border
line_width = cr.get_line_width()
cr.rectangle(-1+line_width, -1+line_width,
log_rect.width+(padding*2)-line_width,
log_rect.height+(padding*2)-line_width)
cr.stroke()
# use the surface as drag icon
Gtk.drag_set_icon_surface(context, surface)

Related

How to Screenshot a Website Element without Collapsible Division using Selenium?

I want to screenshot just the US hot pots map in https://www.nytimes.com/interactive/2021/us/covid-cases.html but the collapsible division at the very bottom (that says Thanks for reading the Times) keeps coming with the screenshot:
How can I exclude that?
Also ideally the New York Time banner at the top would be cropped out. I used Pillow's Image.crop() to crop from the first image captured but wonder if there is a more convenient/elegant way to achieve that. Any thoughts? Thank you!
Here's my code:
from Screenshot import Screenshot
from selenium.webdriver.common.by import By
from selenium import webdriver
from PIL import Image
ob = Screenshot.Screenshot()
driver = webdriver.Chrome()
driver.page_load_strategy = 'none'
url = "https://www.nytimes.com/interactive/2021/us/covid-cases.html"
driver.get(url)
class_name = "mapboxgl-canvas"
element = driver.find_element(By.CLASS_NAME, class_name)
element.screenshot('{}.png'.format(class_name))
location = element.location
size = element.size
print(class_name, 'location:', location, 'size:', size, '\n')
location = element.location
size = element.size
x = location['x']
# y = location['y']
y = 30
w = x + size['width']
h = y + size['height']
# x = 0; y = 10; w = 950; h = 600
fullImg = Image.open("mapboxgl-canvas.png")
cropImg = fullImg.crop((x, y, w, h))
cropImg.save('cropImage.png')
driver.close()
driver.quit()
After tons of trials and errors, finally I got my code to work. The key is to suppress the expandable dock and re-position the capturing window in js.
Attached is the map with top banner and bottom mapbox logo cropped out.
import platform
from Screenshot import Screenshot
from selenium.webdriver.common.by import By
from PIL import Image, ImageDraw
from selenium import webdriver
from selenium.webdriver import ChromeOptions
import numpy as np
#-------------------------------- Part 1 Take a Screenshot of the Map -----------------------------------
#Set up Chrome driver
options = ChromeOptions()
options.headless = True
options.add_argument('--page_load_strategy = none') #suppress browser window
driver = webdriver.Chrome(executable_path = ('/usr/local/bin/' if platform.system() == 'Linux' else '' + 'chromedriver')
, options=options)
# driver.get_window_rect()
_ = driver.set_window_rect(x=0, y=50, width=1000, height=1000)
# driver.execute_script("document.body.style.zoom='145%'")
#Load the website to capture screenshot
url = "https://www.nytimes.com/interactive/2021/us/covid-cases.html"
driver.get(url)
driver.execute_script("""window.scrollTo(0, 2071);
var element = document.querySelector('[data-testid="expanded-dock"]');
element.style.visibility = 'collapse';
element.style.height = '0px';""") #Suppress the randomly pop-out expandable dock at the bottom which messes up the screenshot window
#Identify the element to screenshot, which is the hot spots map
#-------------------------------------------------------------------------------------------------------
#Can't use class_name = "mapboxgl-canary",'multi-map', "map-wrap", "mapboxgl-interactive"
#These class_names are the same as "mapboxgl-canvas": "aspect-ratio-outer", "aspect-ratio-inner", "map", "mapboxgl-map"
#Using ID = "maps" works too, just y location is different. Stick to class_name = "mapboxgl-canvas"
# tag = "maps"
# element = driver.find_element(By.ID, tag)
#-------------------------------------------------------------------------------------------------------
tag = "mapboxgl-canvas"
element = driver.find_element(By.CLASS_NAME, tag)
img_path = '{}.png'.format(tag)
_ = element.screenshot(img_path)
#Check map location and size for window.scrollTo coordinates
location = element.location
size = element.size
print(tag, 'location:', location, 'size:', size, '\n')
# Make sure window.scrollTo = y location = 2382 and height and width stay at 643 and 919 when configuring set_window_rect()
# mapboxgl-canvas location: {'x': 30, 'y': 2382} size: {'height': 643, 'width': 919}
#Crop image to remove unwanted pixels
# x = location['x']
# y = location['y']
x=0; y=30 #Start from a lower position to crop the top banner
w = x + size['width']
h = y + size['height'] - 60 #Subtract from height to remove mapbox logo at the bottom
fullImg = Image.open(img_path)
cropImg = fullImg.crop((x, y, w, h)) #(left, upper, right, lower)-tuple
cropImg.save('cropImage.png')
fullImg.close()
driver.close()
driver.quit()
#--------- Part 2 Mask unwanted parts of the image (top right size control, P.R. region at bottom right) -----------
im = Image.open('cropImage.png')
draw = ImageDraw.Draw(im)
#Vertices of masking rectangles, one for top right size control, the other for bottom right Puerto Rico
top_left = (cropImg.width -41, 0)
bottom_right = (cropImg.width - 8, 40)
top_left2 = (cropImg.width - 100, cropImg.height - 45)
bottom_right2 = (cropImg.width - 40, cropImg.height - 15)
draw.rectangle((top_left, bottom_right), fill=(255, 255, 255))
draw.rectangle((top_left2, bottom_right2), fill=(255, 255, 255))
# Save final image
im.save('cropImage1.png')

issue with update shown values in a pyqt5-applet (using qtableview)

I want to make a qtableview widget correctly updating. I'm working on a calibration applet, where i wanna fill cell by cell of an (e. g.) 100 x 100 x 4 array.
If my hardware reaches position 1, 2, 3, and so on, I will trigger a voltage measurement and gather those values with an i2c-read out-function.
So issues a la "my qtableview is not updating" are omnipresent.
But so far, I'm not able to adapt examples I have read, to make my code behaving as I want.
So if you look at my screenshot:
the problem is:
when I'm clicking on row or col +/-, the yellow highlighting is not changing instantly
when I'm clicking on store i²c, which is meant to put a dummy 0.0 in/on selected cell, this is also not changing instantly
Several methods like telling the model that data has changed, I was not able to implement correctly so far.
Could some of you help me to add a few lines just to force applet to update correctly?
fillCSV_forum.py:
### libraries:
import sys # to use e. g. exit function
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
import pandas as pd # to use pandas tables
import numpy as np # to use numpy arrays
### user-defined header files / modules:
from uLib_coloredWidget import Color # import user-defined functions..
from rndGen import i2c_read # .. see folder
### initial settings:
# general
np.random.seed(4) # if using np.random, then pseudo random values will occure
### globals:
nRow = 5; nCol = 5; nSht = 4 # table dimensions
rowIdx = colIdx = shtIdx = 0 # aux vars to index array
rndArray = np.random.rand(nSht, nRow, nCol) * 4.3 # auxilliary before integrating i2c
tabNames = ["A4", "A5","A6","A7"] # array (list) with tab names
rowIdx = 1; colIdx = 1 # aux vars to index selected cell
### declarations / definitions:
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == Qt.BackgroundRole and index.column() == colIdx and index.row() == rowIdx:
# See below for the data structure.
return QtGui.QColor('yellow')
if role == Qt.DisplayRole:
value = self._data.iloc[index.row(), index.column()]
if isinstance(value, float): # to set fixed DISPLAYED precision of floats
return "%.4f" % value
return str(value)
def rowCount(self, index):
return self._data.shape[0]
def columnCount(self, index):
return self._data.shape[1]
def headerData(self, section, orientation, role):
# section is the index of the column/row.
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
class App(QtWidgets.QMainWindow):
# local variable's declarations
# init
def __init__(self):
super().__init__() # default one
self.setup_main_window() # using helper function to increase readability (function call within self scope)
# setup main window
self.createLayout() # function call to create layouts with widgets
self.post_main_window() # pass edited layouts to main window
# declaration / methods / helper functions
def setup_main_window(self): # to set window's / applet's properties
self.centralwidget = QtWidgets.QWidget()
self.setCentralWidget(self.centralwidget)
self.resize( 800, 400 )
self.setWindowTitle( "# disposition calibration #" )
def post_main_window(self): # to publish edited layouts in app window
self.centralwidget.setLayout(self.lyoOut)
def createLayout(self): # to create layouts with widgets
self.lyoOut = QtWidgets.QVBoxLayout() # declare different layouts
self.lyoIn1 = QtWidgets.QHBoxLayout()
self.lyoIn2 = QtWidgets.QGridLayout()
self.createWidgets() # function call pass widgets to sub-layouts
self.lyoOut.addLayout(self.lyoIn1) # inner layouts got widgets by self.createWidgets()
self.lyoOut.addLayout(self.lyoIn2) # merge edited inner layout in/to outside layout here
def createWidgets(self): # create master-layout's widgets (function calls)
# fill 1st row of ouside layout
self.lyoIn1 = self.createNestedTabs(self.lyoIn1) # function call to create master-tabs
# fill 2nd row of outside layout
self.lyoIn2 = self.createButtons(self.lyoIn2) # function call to create buttons
def createNestedTabs(self, layout2modify): # create 1st tab layer
self.MstTabs = QtWidgets.QTabWidget() # create tabs-widget
self.MstTabs.setTabPosition(QtWidgets.QTabWidget.North) # set it's location
self.MstTabs.addTab(self.createChildTabs(), "data") # add several sub-tab layouts to that widget
self.MstTabs.addTab(Color("orange"), "plot") #
stylesheet = """
QTabBar::tab:selected {background: lightgreen;}
QTabBar::tab:!selected {background: lightyellow;}
"""
self.MstTabs.setStyleSheet(stylesheet)
layout2modify.addWidget(self.MstTabs) # add this tabs-widget to passed-in layout
return layout2modify # return edited layout
def createChildTabs(self): # create 2nd tab layer
self.ChdTabs = QtWidgets.QTabWidget() # create tabs-widget
self.ChdTabs.setTabPosition(QtWidgets.QTabWidget.West) # set it's location
self.ChdTabs.addTab(self.createPandasTables(0), "A4")
self.ChdTabs.addTab(self.createPandasTables(1), "A5")
self.ChdTabs.addTab(self.createPandasTables(2), "A6")
self.ChdTabs.addTab(self.createPandasTables(3), "A7")
return self.ChdTabs # return created widgets
def createPandasTables(self, shtIdx): # to creating and editing pandas tables-widgets
# use indexed (pandas)dataframe sheet values
Lbl = ["a","b","c","d","e"]
self.df = pd.DataFrame(rndArray[shtIdx], columns = Lbl, index = Lbl)
# .. to create a widget
self.table_widget = QtWidgets.QTableView() # create QTableView-Widget
self.model = TableModel(self.df) # make df to user defined table model to use in widgets
self.table_widget.setModel(self.model) # pass created model to created widget
# certain formatings
self.table_widget.resizeColumnsToContents() # set column width to content
self.table_widget.horizontalHeader().setStretchLastSection(True) # strech last column to frame width
self.table_widget.verticalHeader().setStretchLastSection(True) # strech last row to frame height
self.table_widget.setAlternatingRowColors(True) # switch on alternating row highlighting
return self.table_widget # return created widgets
def createButtons(self, layout2modify): # helper function - to create layout's buttons
bStoreI2C = QtWidgets.QPushButton("Store i²c")
bStoreI2C.clicked.connect(lambda:self.storeVal())
bStoreI2C.setStyleSheet("QPushButton::hover"
"{"
"background-color : yellow;"
"}")
layout2modify.addWidget(bStoreI2C, 1, 3, 2, 1)
self.lbl_1 = QtWidgets.QLabel()
self.lbl_1.setText(str(rowIdx))
self.lbl_1.setAlignment(QtCore.Qt.AlignCenter)
layout2modify.addWidget(self.lbl_1, 1, 5, 2, 1)
bRowAdd = QtWidgets.QPushButton("row +")
bRowAdd.clicked.connect(lambda:self.rowAdd())
layout2modify.addWidget(bRowAdd, 2, 6)
bRowSub = QtWidgets.QPushButton("row -")
bRowSub.clicked.connect(lambda:self.rowSub())
layout2modify.addWidget(bRowSub, 1, 6)
return layout2modify # return edited layout
def storeVal(self):
#i2c_vals = get_i2c_values(i2c_addrs)
for i in range (0,4):
#self.tbData[i, rowIdx, colIdx] = i2c_vals[i] # change cell entries with imported value
rndArray[i, rowIdx, colIdx] = 0
#self.tbData[sht, row, col] = 99 # change cell entry with imported value
# try 1
#self.table_widget.update()
#self.table_widget.repaint()
#self.model.select()
#self.table_widget.select()
# try 2
# self.refreshModel() # not working so far
#self.model = TableModel(self.df) # make df to user defined table model to use in widgets
#self.table_widget.setModel(self.model)
# print(rndArray)
print('i²c-value(s) stored')
def rowAdd(self):
global rowIdx
rowIdx = (rowIdx + 1) % nRow # increment and modulo to cycle
self.lbl_1.setText(str(rowIdx)) # update label's text
print('row is ', rowIdx)
def rowSub(self):
global rowIdx
rowIdx = (rowIdx - 1) % nRow # increment and modulo to cycle
self.lbl_1.setText(str(rowIdx)) # update label's text
print('row is ', rowIdx)
### main:
def main():
app = QtWidgets.QApplication(sys.argv) # instanciate app
window = App() # instanciate window
window.show() # show window
app.exec_() # stuck here 'til window is closed
print('# window will be terminated.. #')
time.sleep(2)
print('# ..app execution closed #')
# make file executable
if __name__ == '__main__':
main()
rndGen.py: (is called in fillCSV_forum.py)
import numpy as np
def i2c_read():
floats = np.random.rand(4,1,1) * 4.3
return floats
uLib_coloredWidget.py: (is called in fillCSV_forum.py)
from PyQt5.QtGui import QColor, QPalette
from PyQt5.QtWidgets import QWidget
class Color(QWidget):
def __init__(self, color):
super().__init__()
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(QPalette.Window, QColor(color))
self.setPalette(palette)
pip freeze --local-output of virtual enviroment:
numpy==1.23.0
pandas==1.4.3
PyQt5==5.15.7
PyQt5-Qt5==5.15.2
PyQt5-sip==12.11.0
python-dateutil==2.8.2
pytz==2022.1
six==1.16.0
[... additionally many hours of trial and error....]
i think i finally got a dirty solution / work around..
the problem i could determining, was e. g. if i am clicking the col+/- or store button, the focus of recently selected tab is vanishing.
first when click again into any tab region or select another tabs those values are updating.
so i tried to look for programmatically tab swap and did this as a dirty work around because i could not find a method like "reactivate tab again"
i added ... :
def storeVal(self):
#i2c_vals = get_i2c_values(i2c_addrs)
for i in range (0,nSht):
self.df[i].iat[rowIdx, colIdx] = 99
print('i²c-value(s) stored')
self.show_data()
def show_data(self):
x = self.ChdTabs.currentIndex()
print(x) # debugging
self.ChdTabs.setCurrentIndex(1)
self.ChdTabs.setCurrentIndex(x)
... a show method and called it at the end of the store-method.
in this show method i programmatically swap the active tab back and forth. this is so fast, that i cannot see it
now my values are correctly shown
another tiny if else code is necessary to also swap if tab 1 is selected, but this is cosmetic thing

QApplication.focusWidget().pos() always returning 0

I have a custom QWidget that I have embedded into a QTableWidget.
When I toggle the QCheckBoxes and modify the text in the QLineEdit widgets, the program is not able to distinguish the widgets in rows 2 and 1 from the widgets in row 0. How can I change the program so that it prints the correct row and column of the QLineEdit widget that is being edited or the Checkbox that is being toggled?
Figure 1 shows a screenshot of the program with the output after selecting the third checkbox many times in Visual Studio Code. The output is expected to read “2 0” repeatedly but instead it reads “0 0”.
Figure 2 Similarly, when I modify the text in the QLineEdit in cell 2,0 from “My Custom Text” to “Text” the program prints “Handle Cell Edited 0,0”, although it is expected to print “Handle Cell Edited 2,0 Cell 2,0 was changed to Text”.
Code:
# Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
# https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui
class SimpleTable(QTableWidget):
def __init__(self,window):
# Call the parent constructor
QTableWidget.__init__(self)
self.window = window
class myWidget(QWidget):
#This code is adapted paritally form a post by user sebastian at:
#https://stackoverflow.com/a/29764770/18914416
def __init__(self,parent=None):
super(myWidget,self).__init__()
self.Layout1 = QHBoxLayout()
self.item = QLineEdit("My custom text")
#https://stackabuse.com/working-with-pythons-pyqt-framework/
self.Checkbox = QCheckBox()
self.Checkbox.setCheckState(Qt.CheckState.Unchecked)
self.Layout1.addWidget(self.Checkbox)
self.Layout1.addWidget(self.item)
#https://stackoverflow.com/questions/29764395/adding-multiple-widgets-to-qtablewidget-cell-in-pyqt
self.item.home(True)
#https://www.qtcentre.org/threads/58387-Left-text-alignment-for-long-text-on-QLineEdit
self.setLayout(self.Layout1)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = SimpleTable(window=self)
layout.addWidget(self.table_widget)
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i in range(len(items)):
c = QTableWidgetItem(items[i][0])
m = QTableWidgetItem(items[i][1])
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 1, c)
self.table_widget.setItem(i, 2, m)
myWidget1 = myWidget()
myWidget1.Checkbox.stateChanged.connect(self.handleButtonClicked)
myWidget1.item.editingFinished.connect(self.handle_cell_edited)
self.table_widget.setCellWidget(i,0,myWidget1)
myWidget1.Layout1.setContentsMargins(50*i+10,0,0,0)
self.show()
self.table_widget.itemChanged.connect(self.handle_cell_edited)
def handleButtonClicked(self):
#Adapted from a post by user: Andy at:
# https://stackoverflow.com/a/24149478/18914416
button = QApplication.focusWidget()
# or button = self.sender()
index = self.table_widget.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
# I added this fuction:
def handle_cell_edited(self):
if QApplication.focusWidget() != None:
index = self.table_widget.indexAt(QApplication.focusWidget().pos())
x,y = index.column(),index.row()
if index.isValid():
print("Handle Cell Edited",index.row(), index.column())
if self.table_widget.item(y,x)!= None:
print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")
def main():
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
main()
What I've Tried So Far:
I learned that QT has two types of widgets that can be embedded in a table; a QTableWigetItem which can be inserted into a table using setItem()(3) and Qwidgets, which can be placed into a table using setCellWidget().(4) Generally, I know that using a QTableWigetItem one can set the item.setFlags(Qt.ItemFlag.ItemIsUserCheckable)
flag to create a checkbox in the cell. (3) However, when using the QTableWigetItem, I wasn’t able to find a way to indent the checkboxes. Because giving each checkbox its own indentation level is important in the context of my program, I’ve decided to use Qwidgets instead of QTableWigetItems in the few select cells where indenting is important.
I’ve read that by creating a QItemDelegate(5)(6), you can do a lot more with setting QWidgets in boxes. However, creating a delegate seems complicated, so I’d prefer to avoid this if possible. If there is no other way to make the program register the correct cell number of the cell being edited, creating a delegate will be the next thing I look into.
For anyone who might want to experiment with QTableWigetItems in this application, here is an equivalent program that uses QTableWigetItems instead of QWidgets but doesn't permit separate indentation or editing of the text field in column 0. For either and both of these two reasons, a QTableWigetItem seems not to be usable for the checkboxes in column 0.
Less Successful Attempt using QTableWidgetItem:
#Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
# https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui
class SimpleTable(QTableWidget):
def __init__(self,window):
QTableWidget.__init__(self)
self.window = window
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = SimpleTable(window=self)
layout.addWidget(self.table_widget)
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i in range(len(items)):
c = QTableWidgetItem(items[i][0])
m = QTableWidgetItem(items[i][1])
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 1, c)
self.table_widget.setItem(i, 2, m)
item = QTableWidgetItem("My Custom Text")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable| Qt.ItemFlag.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Unchecked)
self.table_widget.setItem(i,0,item)
#https://youtu.be/DM8Ryoot7MI?t=251
self.show()
#I added this line:
self.table_widget.itemChanged.connect(self.handle_cell_edited)
def handleButtonClicked(self):
#Adapted from a post by user: Andy at:
# https://stackoverflow.com/a/24149478/18914416
button = QApplication.focusWidget()
# or button = self.sender()
index = self.table_widget.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
# I added this fuction:
def handle_cell_edited(self):
if QApplication.focusWidget() != None:
index = self.table_widget.indexAt(QApplication.focusWidget().pos())
x,y = index.column(),index.row()
if index.isValid():
print("Handle Cell Edited",index.row(), index.column())
if self.table_widget.item(y,x)!= None:
print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")
def main():
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
main()
Bibliography:
1.https://i.stack.imgur.com/FudE3.png
2.https://i.stack.imgur.com/C2ypp.png
3.https://youtu.be/DM8Ryoot7MI?t=251
4.https://stackoverflow.com/questions/24148968/how-to-add-multiple-qpushbuttons-to-a-qtableview/24149478#24149478
5.Creating a QItemDelegate for QWidgets, https://stackoverflow.com/a/35418141/18914416
6.Need to create a QItemDelegate to add a stylesheet to QTableWidgetItems: https://forum.qt.io/topic/13124/solved-qtablewidgetitem-set-stylesheet
The geometry of a widget is always relative to its parent.
In your first example, the problem is that the pos() returned for the widget is relative to the myWidget container, and since the vertical position is always a few pixels below the top of the parent (the layout margin), you always get the same value.
The second example has another conceptual problem: the checkbox of a checkable item is not an actual widget, so the widget you get is the table itself.
def handle_cell_edited(self):
# this will print True
print(isinstance(QApplication.focusWidget(), QTableWidget))
As explained above, the geometry is always relative to the parent, so you will actually get the position of the table relative to the window.
The solution to the first case is quite simple, as soon as you understand the relativity of coordinate systems. Note that you shall not rely on the focusWidget() (the widget might not accept focus), but actually get the sender(), which is the object that emitted the signal:
def handleButtonClicked(self):
sender = self.sender()
if not self.table_widget.isAncestorOf(sender):
return
# the widget coordinates must *always* be mapped to the viewport
# of the table, as the headers add margins
pos = sender.mapTo(self.table_widget.viewport(), QPoint())
index = self.table_widget.indexAt(pos)
if index.isValid():
print(index.row(), index.column())
In reality, this might not be that necessary, as an item delegate will suffice if the indentation is the only requirement: the solution is to properly set the option.rect() within initStyleOption() and use a custom role for the indentation:
IndentRole = Qt.UserRole + 1
class IndentDelegate(QStyledItemDelegate):
def initStyleOption(self, opt, index):
super().initStyleOption(opt, index)
indent = index.data(IndentRole)
if indent is not None:
left = min(opt.rect.right(),
opt.rect.x() + indent)
opt.rect.setLeft(left)
class SimpleTable(QTableWidget):
def __init__(self,window):
QTableWidget.__init__(self)
self.window = window
self.setItemDelegateForColumn(0, IndentDelegate(self))
class Window(QWidget):
def __init__(self):
# ...
for i in range(len(items)):
# ...
item.setData(IndentRole, 20 * i)

How can I modify a QtableWidget which is on a QMainWindow from a QGraphicsView method?

Essentially I created a QMainWindow which has a Splitter which, in turn, has on its left side a QTableWidget and on the right side a QGraphicsView.
I have also created a method to zoom in and out in the QGraphicsView. Now, I want to resize the height of the rows depending on the amount of zoom provided by the user.
class MyWindow(QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
# Main characteristics of the window
self.setGeometry(50, 50, 1000, 700)
#User Interface
self.initUI()
def initUI(self):
#Creation of table and timeline splitter
self.table_and_view_splitter = QtWidgets.QSplitter()
self.table_and_view_splitter.setOrientation(QtCore.Qt.Horizontal)
#Creation of metadata table
self.create_table()
self.table_and_view_splitter.addWidget(self.table)
#Creation of View and Scene for timeline
self.create_view()
self.table_and_view_splitter.addWidget(self.view)
# Creation of vertical splitter
self.vertical_splitter = QtWidgets.QSplitter()
self.vertical_splitter.setOrientation(QtCore.Qt.Vertical)
self.vertical_splitter.insertWidget(1, self.table_and_view_splitter)
# Choosing the sizes of the upper and lower widgets of the Qsplitter
self.sizes_list = [100, 5000]
self.vertical_splitter.setSizes(self.sizes_list)
self.setCentralWidget(self.vertical_splitter)
def create_table(self):
self.table = QTableWidget()
self.table.setColumnCount(3)
self.table.setRowCount(100)
for i in range(self.table.rowCount()):
self.table.setRowHeight(i, 10)
def create_view(self):
self.view = viewFor()
self.scene = QtWidgets.QGraphicsScene()
self.scene.addEllipse(1, 1, 10, 10)
self.view.setScene(self.scene)
class viewFor(QGraphicsView):
def __init__(self):
super(viewFor, self).__init__()
self.drag = False
self.setTransformationAnchor(self.NoAnchor)
def wheelEvent(self, event):
self.setTransformationAnchor(self.AnchorUnderMouse)
zoom_in_factor = 1.1
zoom_out_factor = 1 / zoom_in_factor
# Save the scene pos
old_position = self.mapToScene(event.pos())
if QApplication.keyboardModifiers() == Qt.ControlModifier:# CTRL + Scroll -> X and Y Zoom
# Zoom
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, zoom_factor)
#HERE I WANT TO RESIZE THE ROWS HEIGHT ACCORDING TO THE zoom_factor
# Get the new position
new_position = self.mapToScene(event.pos())
# Move scene to old position
delta = new_position - old_position
self.translate(delta.x(), delta.y())
else:# Only Scroll -> only X Zoom
# Zoom
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, 1)
# Get the new position
new_position = self.mapToScene(event.pos())
# Move scene to old position
delta = new_position - old_position
self.translate(delta.x(), delta.y())
app = QApplication([])
foo = MyWindow()
foo.show()
sys.exit(app.exec_())
You need to emit a signal whenever the scale is changed, and QHeaderView.setDefaultSectionSize() for the vertical header. Note that you should probably use setSectionResizeMode(QHeaderView.Fixed) to avoid user resizing (or just leave it to Interactive, but certainly don't use Stretch or ResizeToContents).
You should obviously ensure that the range is valid or find your own algorithm (that sanitizes the value to a valid range that has at least a minimum of 1).
In this case I used the default original value and multiplied it using the scale factor of the view's transformation (see QTransform > rendering graphics about the meaning of the transformation matrix).
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
# Main characteristics of the window
self.setGeometry(50, 50, 1000, 700)
#User Interface
self.initUI()
self.view.scaleChanged.connect(self.resizeRows)
self.defaultSize = self.table.verticalHeader().defaultSectionSize()
def resizeRows(self, scale):
self.table.verticalHeader().setDefaultSectionSize(scale * self.defaultSize)
class viewFor(QtWidgets.QGraphicsView):
scaleChanged = QtCore.pyqtSignal(float)
# ...
def wheelEvent(self, event):
self.setTransformationAnchor(self.AnchorUnderMouse)
zoom_in_factor = 1.1
zoom_out_factor = 1 / zoom_in_factor
# Save the scene pos
old_position = self.mapToScene(event.pos())
if event.modifiers() == QtCore.Qt.ControlModifier:# CTRL + Scroll -> X and Y Zoom
# Zoom
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, zoom_factor)
# emit the signal based on the transformation scale factor
self.scaleChanged.emit(self.transform().m11())
# ...
Note that you don't need to use QApplication.keyboardModifiers, as you can access modifiers() of all keyboard/mouse events.

How do I rebuild a gridLayout during resize?

I am trying to catch resize event of the window, and when I do, basically delete all the widget items in a gridLayout and rebuild the rows/columns to fit the new resized window. I am having trouble getting this to work properly and not sure if this is the best method that I have used. Right now two issues happen:
It doesn't seem to be deleting items, rebuilding and adding the columns properly as I resize the window bigger (some items delete, some get added, but seem to just overlap and never fit to the new window size).
Resize seems to get called on start/creation of the window.
class Window (QtGui.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.thumbs = []
self.thumbWidgets = []
self._resize_timer = None
self.resizeCompleted.connect(self.handleResizeCompleted)
self.setGeometry(100, 100, 800, 600)
self.home()
def home(self):
self.centralwidget = QtGui.QWidget(self)
'''MainLAYOUT
'''
self.mainLayout = QtGui.QVBoxLayout(self.centralwidget)
self.thumb_tab_QGroupBox = QtGui.QGroupBox(self.centralwidget)
'''GroupBoxLAYOUT
'''
self.vLayout = QtGui.QVBoxLayout(self.thumb_tab_QGroupBox)
self.vLayout.setObjectName("GroupVLayout")
#Scroll Area
self.thumbScrollArea = QtGui.QScrollArea(self.thumb_tab_QGroupBox)
self.thumbScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.thumbScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.thumbScrollArea.setWidgetResizable(True)
self.thumbScrollArea.setAlignment(QtCore.Qt.AlignLeft)
self.thumbScrollArea.setObjectName("thumb_scrollArea")
self.scrollAreaWidgetContents = QtGui.QWidget()
self.scrollAreaWidgetContents.setMinimumSize(QtCore.QSize(840, scrollAreaX))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.thumbScrollArea.setWidget(self.scrollAreaWidgetContents)
self.vLayout.addWidget(self.thumbScrollArea)
self.mainLayout.addWidget(self.thumb_tab_QGroupBox)
#Grid in Scroll Area
self.gridLayoutWidget = QtGui.QWidget(self.scrollAreaWidgetContents)
self.gridLayoutWidget.setObjectName("gridLayoutWidget")
self.gridLayout_QGridLayout = QtGui.QGridLayout(self.gridLayoutWidget)
self.gridLayout_QGridLayout.setObjectName("gridLayout")
#Loads thumbnails
self.getThumbnails()
self.mainLayout.setAlignment(QtCore.Qt.AlignLeft)
self.setCentralWidget(self.centralwidget)
def resizeEvent(self, resizeEvent):
self.updateResizeTimer(300)
def updateResizeTimer(self, interval=None):
if self._resize_timer is not None:
self.killTimer(self._resize_timer)
if interval is not None:
self._resize_timer = self.startTimer(interval)
else:
self._resize_timer = None
def timerEvent(self, event):
if event.timerId() == self._resize_timer:
self.updateResizeTimer()
self.resizeCompleted.emit()
def handleResizeCompleted(self):
print('resize complete')
# Get new window size on resize
width = self.centralwidget.frameGeometry().width()
height = self.centralwidget.frameGeometry().height()
thumbsPerRow = width / 200
print "numThumbnails per Width", thumbsPerRow
self.gridLayoutWidget.adjustSize()
self.gridLayout_QGridLayout.maximumSize()
for widget in self.thumbWidgets:
print "Removing widget", widget
self.gridLayout_QGridLayout.removeWidget(widget)
#widget.deleteLater()
self.populate(self.thumbWidgets, QtCore.QSize(200,200), thumbsPerRow)
def queryThumbnailCount(self):
....
...
..
return sizeX
def getThumbnails(self):
.....
....
...
.
self.createThumbWidgets(self.thumbs, QtCore.QSize(200,200))
self.populate(self.thumbs, QtCore.QSize(200,200))
def createThumbWidgets(self, pics, size, imagesPerRow=4, flags=QtCore.Qt.KeepAspectRatioByExpanding):
for pic in pics:
label = QtGui.QLabel("")
pixmap = QtGui.QPixmap(pic)
pixmap = pixmap.scaled(size, flags)
label.setPixmap(pixmap)
self.thumbWidgets.append(label)
#Add thumbnails to grid
def populate(self, pics, size, imagesPerRow=6, flags=QtCore.Qt.KeepAspectRatioByExpanding):
row = col = 0
for widget in self.thumbWidgets:
print "Adding Image to column "+str(col)
self.gridLayout_QGridLayout.addWidget(widget, row, col)
col +=1
if col % imagesPerRow == 0:
row += 1
col = 0
GUI = Window()
GUI.show()
This might be better achieved with a QGraphicsView.
Create a subclass of a QGraphicsView that also creates a QGraphicsScene for itself. Have it store the list of pixmaps you want it to display. Override the resizeEvent in your subclass and have it clear the QGraphicsScene and re-add all the pixmaps to the scene using QGraphicsPixmapItems. Before you add them to the scene, get the total width and height from the QGraphicsView.viewport(). You can get the individual pixmap width/height by dividing by rows/columns. Then scale each pixmap before you add it to the scene.