I have a problem related to this question and the script given as the answer. I can add some functionalities to Attribute Table (AT) by the script in the post. But according to the post, I have to open the AT using showAttributeTable
method, because the method returns the reference of the AT created/opened.
Of course, I can get references of all opened ATs using the following line.
tables = [w for w in qApp.allWidgets() if w.objectName() == 'AttributeTable']
Let's say, I want to add a button to all ATs to be opened or sort by a column for a specific layer. Somehow, I think I need to catch "the AT's opening event" or "a widget/child was added to iface.mainWindow()
" etc.
I have looked at Qt5 and QGIS API(QgsApplication) documentation, but I couldn't find anything helpful or I missed something.
A pseudo code for a possible solution:
def something_opened(something):
if something is an attribute_table and active_layer is foo_bar:
do something
main_window.addedsomething.connect(something_opened)
Note: @Ben's answer is pretty nice. But, it still requires me to focus on the AT.
2 Answers 2
Update- March 2025
While the original answer below will still work when opening the attribute table as an undocked floating window, I have recently revisited this issue and found that a better solution is to install an event filter on the main window and intercept QEvent.ChildAdded
. This has the advantage that you do not need to have focus on the attribute table so it will work whether the attribute table is opened either floating or docked (since in more recent versions the attribute table can be opened as docked by default).
I tested the example below as a startup.py
script:
from qgis.utils import iface
from qgis.PyQt.QtCore import QObject, QEvent
from qgis.PyQt.QtWidgets import QAction
class AddAttributeTableAction(QObject):
def __init__(self, iface):
super().__init__()
self.iface = iface
self.main_window = self.iface.mainWindow()
self.main_window.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == QEvent.ChildAdded and obj == self.main_window:
self.attribute_dialog_opened()
return super(AddAttributeTableAction, self).eventFilter(obj, event)
def attribute_dialog_opened(self):
tbl_dlgs = [w for w in self.main_window.children() if 'QgsAttributeTableDialog' in w.objectName()]
if tbl_dlgs:
table_dialog = tbl_dlgs[0]
toolbar = [c for c in table_dialog.children() if c.objectName() == 'mToolbar'][0]
# check if action has already been added to toolbar
already_exists = [a for a in toolbar.actions() if a.objectName() == 'TestAction']
if not already_exists:
new_button = QAction('Test', table_dialog)
new_button.setObjectName('TestAction')
toolbar.addAction(new_button)
new_button.triggered.connect(self.run_action)
def run_action(self):
'''Simple method to test action'''
layer = iface.activeLayer()
layer.selectByIds([1])
Test = AddAttributeTableAction(iface)
Original answer:
Interesting question! I couldn't find any native signal emitted when an attribute table is opened or closed so I would call this solution a fairly inelegant workaround but it seems to work well enough. I found that QApplication
has a focusChanged(old, new)
signal which is emitted whenever the widget focus changes e.g. opening/ closing dialogs or clicking between non-modal windows etc. and returns the old and new widget objects.
class addAttributeTableAction(object):
def __init__(self, app):
self.app = app
self.app.focusChanged.connect(self.attribute_dialog_opened)
def __del__(self):
self.app.focusChanged.disconnect(self.attribute_dialog_opened)
def attribute_dialog_opened(self, old, new):
if isinstance(new, QTableView):
#I don't like the line below but I could't think of a better/quicker way to
#return the Dialog object from the QTableView object returned by the 'new' parameter
#of the focusChanged signal
table_dialog = new.parent().parent().parent().parent()
toolbar = [c for c in table_dialog.children() if isinstance(c, QToolBar)][0]
# check if action has already been added to toolbar
already_exists = [a for a in toolbar.actions() if a.objectName() == 'TestAction']
if not already_exists:
new_button = QAction('Test', table_dialog)
new_button.setObjectName('TestAction')
toolbar.addAction(new_button)
new_button.triggered.connect(self.run_action)
def run_action(self):
'''Simple method to test action'''
layer = iface.activeLayer()
layer.selectByIds([1])
Test = addAttributeTableAction(qApp)
#Uncomment below andcomment above to stop listening for focusChanged signal
#del Test
Quick demo:
-
This answer seems the best solution I found so far. I used
evetFilter
but I couldn't get any result. Probably I miss something.Kadir Şahbaz– Kadir Şahbaz2020年04月26日 13:52:45 +00:00Commented Apr 26, 2020 at 13:52 -
Hi @KadirŞahbaz, some years have passed but I've recently re-visited this and have updated my answer with a solution using an event filter.Ben W– Ben W2025年03月23日 04:37:34 +00:00Commented Mar 23 at 4:37
Here is my solution (QGIS 3.12.2, works perfectly fine under Ubuntu, I saw few QGIS crashes under Windows 10) that gives you access directly to all opened attribute tables dialogs or attribute tables of a specific layer.
Explanations :
- Copy and run this code into the QGIS Python Console Editor
- The code run in another thread, so, you can continue QGIS activities.
- Modify the refresh rate if you need. Here it is 2 seconds.
- If you want to access to attribute tables :
attribute_tables()
returns a list with all opened attribute tables ;attribute_tables(layer_id)
returns the list of all opened attribute tables of the layer.
- If you want to stop the worker (= background thread), just
worker = None
#!/usr/bin/env python3
from time import sleep
from PyQt5.QtCore import QThread, pyqtSignal
from qgis.core import QgsApplication, QgsProject
class Worker(QThread):
result = pyqtSignal(dict)
def __init__(self, qgsapp, qgsproject):
super(Worker, self).__init__()
self.app = qgsapp
self.qpj = qgsproject
def __del__(self):
self.wait()
def run(self):
while True:
ats = {
id(o): [o.objectName()[24:], o]
for o in self.app.allWidgets()
if isinstance(o, QDialog)
and o.objectName()[:24] == "QgsAttributeTableDialog/"
}
lyr_at = {}
for lyrid, dialog in ats.values():
if lyrid in lyr_at:
lyr_at[lyrid].append(dialog)
else:
lyr_at[lyrid] = [dialog]
self.result.emit(lyr_at)
sleep(2) # refresh rate : 2 seconds
class Atat(object):
def __init__(self):
self._dialogs = {}
@property
def dialogs(self) -> dict:
return self._dialogs
@dialogs.setter
def dialogs(self, value: dict):
if isinstance(value, dict):
self._dialogs = value
def attribute_tables(layer_id: str = None) -> list:
global ATINST
if not layer_id:
return [d for ld in ATINST.dialogs.values() for d in ld]
elif layer_id in ATINST.dialogs:
return ATINST.dialogs[layer_id]
return []
app = QgsApplication.instance()
qpj = QgsProject.instance()
ATINST = Atat()
worker = Worker(app, qpj)
worker.result.connect(
lambda new_at: setattr(ATINST, 'dialogs', new_at)
)
worker.start()