Russian Qt Forum

Программирование => Python => Тема начата: RockBomber от Март 30, 2012, 17:20



Название: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Март 30, 2012, 17:20
Переползаю с универских познаний Delphi на более-менее сурёзное программрование.
Понадобилось тут переопределить правый клик мыши по QLineEdit и QTextEdit, чтобы выделялся в них весь текст и появлялось контекстное меню с одним лишь пунктом "Копировать". Вот сделал такой вариант с множественном наследованием. Все работает. Но гложат сомнения, все ли правильно сделал?

Код
Python
class MyQWidget(QWidget):
   def __init__(self):
       QWidget.__init__(self)
       self.menu = QMenu(self)
       self.menu.addAction(tr('Copy'))
       self.menu.triggered.connect(self.copy)
 
   def mousePressEvent(self, event):
       self.parent.__self__.mousePressEvent(event)
       if event.button() == Qt.RightButton:
           self.selectAll()
           self.menu.exec_(event.globalPos())
 
 
class MyQLineEdit(QLineEdit, MyQWidget): pass
 
 
class MyQTextEdit(QTextEdit, MyQWidget): pass
 


Название: Re: Множественное наследование в PyQt для чайников. Со свистком.
Отправлено: kambala от Март 30, 2012, 17:36
может проще просто подконнектить сигнал contextMenuRequested нужных объектов в слот, где будет создаваться меню?


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Март 31, 2012, 20:46
Спасибо. Заставили поработать головой) Действительно, Custom Context Menu выглядит более удачным решением, чем переопределение методов класса.
Набросал такой код, работает. Но если для QLineEdit меню появляется нормально, то для QTextEdit оно появляется выше курсора.
Код
Python
   def __init__(self)
       lineEdit = QLineEdit()
       lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
       lineEdit.customContextMenuRequested.connect(self.copy_context_menu)
 
       textEdit = QTextEdit()
       textEdit.setContextMenuPolicy(Qt.CustomContextMenu)
       textEdit.customContextMenuRequested.connect(self.copy_context_menu)
 
       self.copy_all_action = QAction(tr('Copy'), self)
       self.copy_all_action.triggered.connect(self.copy_action)
 
   def copy_context_menu(self, pos):
       menu = QMenu(self)
       menu.addAction(self.copy_all_action)
       menu.exec_(self.mapToGlobal(pos))
 
   def copy_action(self):
       self.focusWidget().selectAll()
       self.focusWidget().copy()
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Март 31, 2012, 21:13
Разобрался с появлением меню над курсором для QTextEdit. Нужно вызывать метод mapToGlobal() не основного виджета, а того, по которому произвели клик. Вообще правильно ли определять виджет, у которого вызвали контекстное меню, методом focusWidget() ? В документации другого способа не увидел(
Код
Python
   def copy_context_menu(self, pos):
       menu = QMenu()
       menu.addAction(self.copy_all_action)
       menu.exec_(self.focusWidget().mapToGlobal(pos))
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: iroln от Апрель 01, 2012, 09:06
Кто послал сигнал, можно узнать через sender.

Код
Python
def copy_context_menu(self, pos):
   menu = QMenu()
   menu.addAction(self.copy_all_action)
   menu.exec_(self.sender().mapToGlobal(pos))
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: iroln от Апрель 01, 2012, 09:14
А вообще вашу задачу надо не так решать.

Необходимо установить фильтр событий на те объекты, в которых вы хотите изменить контекстное меню. И обрабатывать всё в функции eventFilter.

Как пример для вашего кода:

Код
Python
class SomeClass(QWidget):
   def __init__(self)
       super(SomeClass, self).__init__()
 
       self.lineEdit = QLineEdit()
       self.textEdit = QTextEdit()
 
       self.lineEdit.installEventFilter(self)
       self.textEdit.installEventFilter(self)
 
       self.copy_all_action = QAction(self.tr('Copy'), self)
       self.copy_all_action.triggered.connect(self.copy_action)
 
 
   def eventFilter(self, obj, event):
       if event.type() == QEvent.ContextMenu:
           menu = QMenu(self)
           menu.addAction(self.copy_all_action)
           menu.exec_(event.globalPos())
           return True
       return False
 

В этом случае вам даже не нужно знать того, кто создал событие, но если потребуется, в метод eventFilter первым (вторым не считая self) аргументом приходит ссылка на объект (obj). Проверка на тип объекта может потребоваться, если у вас фильтр установлен на разные объекты, и вы ловите разные события, тогда надо проверять объект:

Код
Python
def eventFilter(self, obj, event):
   if obj is someObj and event.type() == someEventType:
       # some code
   return False
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Апрель 02, 2012, 14:09
Да, совсем забыл, что виджеты еще и от такого мощного класса QObject наследуются)
Подправил свой код:
Код
Python
class WikiReport(QDialog):
   def __init__(self):
       super(QDialog, self).__init__()
 
       self.lineEdit = QLineEdit(self)
       self.textEdit = QTextEdit(self)
 
       self.lineEdit.installEventFilter(self)
       self.textEdit.installEventFilter(self)
 
       self.copy_all_action = QAction(self.tr('Copy All'), self)
       self.copy_all_action.triggered.connect(self.copy_action)
 
   def eventFilter(self, obj, event):
       print type(obj), type(event)
       if event.type() == QEvent.ContextMenu:
           menu = QMenu(self)
           menu.addAction(self.copy_all_action)
           menu.exec_(event.globalPos())
           return True
       return False
 
   def copy_action(self):
       self.sender().selectAll()
       self.sender().copy()
 
Но что-то я делаю не так.
При вызове меню, sender() возвращает ссылку на виджет, по которому был произведен клик и меню появляется где надо.
Но при использования в слоте copy_action() он возвращает, соответственно, QAction, и возникает исключение "AttributeError: 'QAction' object has no attribute 'selectAll'".

И ещё у меня по какой-то причине для QTextEdit не возникает событие QContextMenuEvent. Хотя для QLineEdit все нормально обрабатывается.

P.S. В данном случае super(SomeClass, self).__init__() абсолютно эквивалентен SomeClass.__init__(self) ? Хотя это уже отдельная тема, на хабре был хороший топик про множественное наследование, алгоритм MRO С3 и линеаризацию. Надо будет перечитать)

В итоге пока такой код рабочий:
Код
Python
class WikiReport(QDialog):
   def __init__(self):
       QDialog.__init__(self)
 
       self.copy_all_action = QAction(self.tr('Copy All'), self)
       self.copy_all_action.triggered.connect(self.copy_action)
 
       menu = QMenu()
       menu.addAction(self.copy_all_action)
       exec_ctx_menu = lambda pos: menu.exec_(self.sender().mapToGlobal(pos))
 
       self.lineEdit = QLineEdit()
       self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
       self.lineEdit.customContextMenuRequested.connect(exec_ctx_menu)
 
       self.textEdit = QTextEdit()
       self.textEdit.setContextMenuPolicy(Qt.CustomContextMenu)
       self.textEdit.customContextMenuRequested.connect(exec_ctx_menu)
 
   def copy_action(self):
       self.focusWidget().selectAll()
       self.focusWidget().copy()
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: iroln от Апрель 02, 2012, 15:15
Цитировать
При вызове меню, sender() возвращает ссылку на виджет, по которому был произведен клик и меню появляется где надо.
Но при использования в слоте copy_action() он возвращает, соответственно, QAction, и возникает исключение "AttributeError: 'QAction' object has no attribute 'selectAll'".
Потому что вызываете его не в слоте, который вызвали, а в слоте, который вызывается из QAction, вот он его и возвращает. Зачем вам sender при использовании eventFilter? Передавайте в copy_action объект, который сгенерировал событие (obj). Можете через поле класса его передавать, ну то есть сохранять ссылку как поле класса, а в функции copy_action её использовать.

Цитировать
И ещё у меня по какой-то причине для QTextEdit не возникает событие QContextMenuEvent. Хотя для QLineEdit все нормально обрабатывается.
Сделайте для textEdit вот так:
Код
Python
self.textEdit.viewport().installEventFilter(self)
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Апрель 02, 2012, 15:48
Действительно, sender() тут и не нужен. С QLineEdir все получилось. А с QTextEdit опять проблема.
Используя self.textEdit.viewport() , контекстное меню подключилось, но тогда в obj передается ссылка не на QTextEdit, а на QWidget. А в этом случае уже не работает obj.selectAll() и obj.copy().

Вот получившийся код:
Код
Python
class WikiReport(QDialog):
   def __init__(self):
       super(QDialog, self).__init__()
 
       self.lineEdit = QLineEdit(self)
       self.textEdit = QTextEdit(self)
 
       self.lineEdit.installEventFilter(self)
       self.textEdit.viewport().installEventFilter(self)
 
   def eventFilter(self, obj, event):
       if event.type() == QEvent.ContextMenu:
           copy_all_action = QAction(self.tr('Copy All'), self)
           copy_all_action.triggered.connect(lambda: self.copy_action(obj))
 
           menu = QMenu(self)
           menu.addAction(copy_all_action)
           menu.exec_(event.globalPos())
           return True
       return False
 
   def copy_action(self, obj):
       obj.selectAll()
       obj.copy()
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: iroln от Апрель 02, 2012, 16:17
Ну в этом случае можно костыль проверку добавить :)

Код
Python
def eventFilter(self, obj, event):
   if event.type() == QEvent.ContextMenu:
       if obj is self.textEdit.viewport():
           obj = self.textEdit
 
       copy_all_action = QAction(self.tr('Copy All'), self)
       copy_all_action.triggered.connect(lambda: self.copy_action(obj))
 
       menu = QMenu(self)
       menu.addAction(copy_all_action)
       menu.exec_(event.globalPos())
       return True
   return False
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Апрель 02, 2012, 16:52
Спасибо, но проверку немного переделал, чтоб не каждый экземпляр QTextEdit таким образом проверять)
Код
Python
           if isinstance(obj.parentWidget(), QTextEdit):
               copy_lambda = lambda: self.copy_action(obj.parentWidget())
           else:
               copy_lambda = lambda: self.copy_action(obj)
 


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: iroln от Апрель 02, 2012, 17:06
Цитировать
чтоб не каждый экземпляр QTextEdit таким образом проверять
Получается что, таким образом вы проверяете каждый объект и это правильно, если у вас много QTextEdit, а не один, как в случае с моей проверкой.


Название: Re: Custom Context Menu в PyQt для чайников. Со свистком.
Отправлено: RockBomber от Апрель 02, 2012, 19:22
Я и имел это в виду, но некорректно выразил мысль) Чтобы для каждого экземпляра QTextEdit не писать отдельную проверку.
Но местами пишут, что проверка объекта на принадлежность классу - дурной тон, и лучше реализовывать свой алгоритм по другому, если возможно.