17

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.

asked Dec 1, 2019 at 2:48
0

2 Answers 2

18
+50

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:

enter image description here

answered Dec 1, 2019 at 9:42
2
  • This answer seems the best solution I found so far. I used evetFilter but I couldn't get any result. Probably I miss something. Commented 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. Commented Mar 23 at 4:37
8

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()
answered Apr 22, 2020 at 16:23

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.