Russian Qt Forum
Ноябрь 22, 2024, 17:54 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: [1]   Вниз
  Печать  
Автор Тема: Custom Context Menu в PyQt для чайников. Со свистком.  (Прочитано 16895 раз)
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
 
« Последнее редактирование: Март 31, 2012, 20:46 от RockBomber » Записан
kambala
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4747



Просмотр профиля WWW
« Ответ #1 : Март 30, 2012, 17:36 »

может проще просто подконнектить сигнал contextMenuRequested нужных объектов в слот, где будет создаваться меню?
Записан

Изучением C++ вымощена дорога в Qt.

UTF-8 has been around since 1993 and Unicode 2.0 since 1996; if you have created any 8-bit character content since 1996 in anything other than UTF-8, then I hate you. © Matt Gallagher
RockBomber
Гость
« Ответ #2 : Март 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()
 
Записан
RockBomber
Гость
« Ответ #3 : Март 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))
 
Записан
iroln
Гость
« Ответ #4 : Апрель 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))
 
Записан
iroln
Гость
« Ответ #5 : Апрель 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
 
« Последнее редактирование: Апрель 01, 2012, 09:36 от iroln » Записан
RockBomber
Гость
« Ответ #6 : Апрель 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()
 
Записан
iroln
Гость
« Ответ #7 : Апрель 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)
 
Записан
RockBomber
Гость
« Ответ #8 : Апрель 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()
 
Записан
iroln
Гость
« Ответ #9 : Апрель 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
 
Записан
RockBomber
Гость
« Ответ #10 : Апрель 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)
 
Записан
iroln
Гость
« Ответ #11 : Апрель 02, 2012, 17:06 »

Цитировать
чтоб не каждый экземпляр QTextEdit таким образом проверять
Получается что, таким образом вы проверяете каждый объект и это правильно, если у вас много QTextEdit, а не один, как в случае с моей проверкой.
Записан
RockBomber
Гость
« Ответ #12 : Апрель 02, 2012, 19:22 »

Я и имел это в виду, но некорректно выразил мысль) Чтобы для каждого экземпляра QTextEdit не писать отдельную проверку.
Но местами пишут, что проверка объекта на принадлежность классу - дурной тон, и лучше реализовывать свой алгоритм по другому, если возможно.
« Последнее редактирование: Апрель 02, 2012, 19:32 от RockBomber » Записан
Страниц: [1]   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.157 секунд. Запросов: 23.