第十章 纯文本编辑自定义上下文菜单界面与功能实现

在这一章我们会学习如何通过自定义上下文菜单来实现和win10上下文菜单相似的效果和功能。

效果演示:后续会陆续更新

学习完本章,你将学会或了解如下知识:

  1. 默认上下文菜单的实现和调用
  2. 实现自定义上下文菜单
  3. 使用默认浏览器的Bing搜索引擎搜索内容

10.1 重点知识

关于自定义上下文菜单我们需要知道如下:

  1. 是什么?
  2. 怎么实现?

上下文菜单是右键单击应用界面某处(或按下快捷键,例如 Windows 上的 Shift + F10)时出现的弹出菜单。

关于实现我们需要了解如下:

  1. QWidget.setContextMenuPolicy()方法:基础窗口,设置上下文菜单策略
  2. PySide6.QtCore.Qt.ContextMenuPolicy: 枚举类型,上下文菜单策略
  3. QWidget.customContextMenuRequested(pos)信号:当1设置为自定义上下文菜单时会触发此信号

简单解释一下:
首先,基于QWidget的窗口触发上下文菜单的时候,如果不做修改就是使用默认的上下文菜单(Qt.DefaultContextMenu),这时会调用contextMenuEvent(event)槽函数。

想要显示自定义的上下文菜单:

  1. 先设置上下文菜单策略为: 自定义菜单(Qt.CustomContextMenu)
  2. 自定义上下文菜单请求信号连接好自定义的上下文菜单槽函数

注意: 继承自QWidgetQAbstractScrollArea及其子类无法使用上诉方法实现。

上下文菜单策略与对应意义一览表:

策略 意义
Qt.NoContextMenu 小部件没有上下文菜单,上下文菜单处理将推迟到小部件的父级。
Qt.PreventContextMen 该小组件没有上下文菜单,并且与 NoContextMenu 相比,处理不会延迟到小组件的父级。这意味着所有鼠标右键事件都保证通过 QWidget.mousePressEvent() QWidget.mouseReleaseEvent() 传递到小部件本身。
Qt.DefaultContextMenu 默认上下文菜单,调用小部件的 QWidget.contextMenuEvent()处理程序
Qt.ActionsContextMenu Actions 上下文菜单,小部件将其 QWidget.actions() 显示为上下文菜单。
Qt.CustomContextMenu 自定义上下文菜单,该小部件发出 QWidget.customContextMenuRequested() 信号。

注意: 部分版本比如PySide6.10可能需要全称才能使用,如Qt.ContextMenuPolicy.CustomContextMenu

10.1.1 简单实现

from PySide6.QtWidgets import QApplication, QWidget, QMenu
from PySide6.QtCore import Qt,QPoint
import sys

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()

        # 设置上下文菜单策略 为 自定义上下文菜单
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

        self.set_event_bind()

    def set_event_bind(self):
        """设置事件绑定
        """
        # 自定义上下文菜单请求信号 连接 自定义上下文菜单 槽函数
        self.customContextMenuRequested.connect(self.show_custom_content_menu)
        
    def show_custom_content_menu(self, pos:QPoint):
        """显示自定义上下文菜单

        :param pos: customContextMenuRequested 传出的 QPoint 位置
        """
        menu = QMenu(self)
        menu.addAction("123")
        menu.addAction("321")
        menu.exec(pos)
    
if __name__ == "__main__":
   app = QApplication(sys.argv)
   window = MyWidget()
   window.show()
   app.exec()

10.2 界面分析

关于win10记事本的上下文菜单:
上下文菜单

基本和菜单项差不多

注意:尽管笔者已经努力而且python for qt也够强大,但是还是存在不足之处:从全选到使用Bing搜索之间的暂时没办法实现。

10.2.1 界面实现

这里将使用俩个自定义类:自定义上下文菜单类与自定义纯文本编辑类。
因为是类实现,所以事件绑定就成为了一个问题,这也是代码责任划分的意义。因为上下文菜单是基于纯文本编辑来实现,所以我们就在自定义纯文本编辑类里实现事件绑定。
代码如下:
custome_content_menu.py

from PySide6.QtWidgets import QMenu

class ContentMenu(QMenu):
    """上下文菜单

    :param QMenu: PySide6 菜单类
    """
    def __init__(self,parent=None):
        """初始化"""
        super().__init__(parent)
        self.setup_ui()
    
    def setup_ui(self):
        """设置界面"""
        self.addAction("撤销(&U)")
        self.addSeparator()
        self.addAction("剪切(&T)")
        self.addAction("复制(&C)")
        self.addAction("粘贴(&P)")
        self.addAction("删除(&D)")
        self.addSeparator()
        self.addAction("全选(&A)")
        self.addSeparator()
        self.addAction("使用Bing搜索(&B)")

custome_plaintext_edit.py

from PySide6.QtWidgets import QPlainTextEdit
from PySide6.QtCore import QPoint,Qt
from custome_content_menu import ContentMenu

class PlainTextEdit(QPlainTextEdit):
    """自定义纯文本编辑

    :param QPlainTextEdit: PySide6 纯文本编辑
    """
    def __init__(self):
        """初始化"""
        super().__init__()
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_custome_content_menu)
    
    def show_custome_content_menu(self,pos: QPoint):
        """显示自定义上下文菜单

        :param pos: customContextMenuRequested 传出的 QPoint 位置
        """
        menu = ContentMenu(self)
        menu.exec(pos)

notepad_main.py

from PySide6.QtWidgets import QMainWindow,QFrame,QApplication
from custome_plaintext_edit import PlainTextEdit
import sys

class NotepadMain(QMainWindow):
    """记事本主界面

    :param QMainWindow: 主窗口
    """
    def __init__(self): 
        """初始化"""
        super().__init__()
        self.setup_ui()

    def setup_ui(self):
        """设置用户界面"""
        # 设置窗口标题
        self.setWindowTitle("无标题 - 记事本")

        # 设置窗口大小
        self.resize(800, 500)
        
        # 创建菜单栏 设置为私有属性
        self.__menubar = self.menuBar()
        
        # 示例化纯文本编辑 
        self.__plain_text_edit = PlainTextEdit()
        
        # 消除框线
        self.__plain_text_edit.setFrameShape(QFrame.Shape.NoFrame)

        # 添加纯文本编辑到 中心窗口
        self.setCentralWidget(self.__plain_text_edit)

        # 添加状态栏目
        self.status_bar = self.statusBar()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    # 添加其他菜单
    notepad_main = NotepadMain()
    notepad_main.show()
    sys.exit(app.exec())

10.3 功能分析

功能分为俩类:

  1. 更新行为状态
  2. 行为触发

1主要就是撤销、剪切、复制、粘贴、删除、全选 这些行为默认不可用,使用Bing搜索默认就是可用的。
2除使用Bing搜索以外都有定义好的函数

10.3.1 更新行为状态

更新行为状态分析

更新策略:

  • 撤销: 撤销可用(QPlainTextEdit.undoAvailable)信号 -连接- 行为对应的setEnabled函数
  • 剪切、复制、删除:都是有选中文本=> 复制可用(QPlainTextEdit.copyAvailable)信号 -连接- 行为对应的setEnabled函数
  • 粘贴: 根据剪贴板是否有内容来实现 => 数据改变(clipbaord.dataChanged)信号(还需要自定义槽函数)
  • 全选:根据文本编辑是否有内容来实现 =>自定义信号(Signal)来判断是否有文本
  • 使用Bing搜索: 默认可用

注意undoAvailablecopyAvailable信号会发射一个是否可用的布尔值正好与setEnabled需要的值对应,所以无需传入参数。

注意: 剪贴板需要使用QGuiApplication.clipboard()来实例化

如何更新粘贴行为的状态? 通过判断剪贴板是否有内容并更新行为状态来实现。

    def reset_paste_state(self):
        """重新设置粘贴状态"""
	# 获取剪贴板文本
        clipbaord_text = self.clipbaord_.text()
	# 非空可用 为空不可用
        if clipbaord_text:
            self.menu.paste_action.setEnabled(True)
        else:
            self.menu.paste_action.setEnabled(False)

如何自定义文本信号来检测纯文本编辑是否有内容?见代码

from PySide6.QtWidgets import QPlainTextEdit
from PySide6.QtGui import QGuiApplication
from PySide6.QtCore import QPoint,Qt,Signal

class PlainTextEdit(QPlainTextEdit):
    """自定义纯文本编辑

    :param QPlainTextEdit: PySide6 纯文本编辑
    """
    hasText = Signal(bool)

    def __init__(self):
        """初始化"""
        super().__init__()
	self.textChanged.connect(self.__has_text)

    def __has_text(self) -> bool:
        """判断是否有文本
        """
        self.hasText.emit(True) if self.toPlainText() else self.hasText.emit(False)
更新行为状态代码

custome_content_menu.py

from PySide6.QtWidgets import QMenu

class ContentMenu(QMenu):
    """上下文菜单

    :param QMenu: PySide6 菜单类
    """
    def __init__(self,parent=None):
        """初始化"""
        super().__init__(parent)
        self.setup_ui()
    
    def setup_ui(self):
        """设置界面"""
        self.undo_action = self.addAction("撤销(&U)")
        self.addSeparator()
        self.cut_action = self.addAction("剪切(&T)")
        self.copy_action = self.addAction("复制(&C)")
        self.paste_action =  self.addAction("粘贴(&P)")
        self.delete_action = self.addAction("删除(&D)")
        self.addSeparator()
        self.select_all =  self.addAction("全选(&A)")
        self.addSeparator()
        self.addAction("使用Bing搜索(&B)")
        
        # 默认 撤销 剪切  复制 粘贴 删除 全选 不可用
        self.undo_action.setEnabled(False)
        self.cut_action.setEnabled(False)
        self.copy_action.setEnabled(False)
        self.paste_action.setEnabled(False)
        self.delete_action.setEnabled(False)
        self.select_all.setEnabled(False)

custome_plaintext_edit.py

from PySide6.QtWidgets import QPlainTextEdit
from PySide6.QtGui import QGuiApplication
from PySide6.QtCore import QPoint,Qt,Signal
from custome_content_menu import ContentMenu

class PlainTextEdit(QPlainTextEdit):
    """自定义纯文本编辑

    :param QPlainTextEdit: PySide6 纯文本编辑
    """
    hasText = Signal(bool)

    def __init__(self):
        """初始化"""
        super().__init__()
        self.__init_signal_event()
        self.__init_content_menu_event()

        # 初始化一个 剪贴板
        self.clipbaord_ = QGuiApplication.clipboard()
        self.menu = ContentMenu(self)

    def __init_signal_event(self):
        """初始化"""
        self.textChanged.connect(self.__has_text)

    def __has_text(self) -> bool:
        """判断是否有文本
        """
        self.hasText.emit(True) if self.toPlainText() else self.hasText.emit(False)

    def __init_content_menu_event(self):
        """初始化上下文菜单"""
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_custome_content_menu)

    def show_custome_content_menu(self,pos: QPoint):
        """显示自定义上下文菜单

        :param pos: customContextMenuRequested 传出的 QPoint 位置
        """
        self.menu.exec(pos)

    def set_event_bind(self):
        """设置事件绑定"""
        # 设置状态 撤销、剪切、复制、粘贴、删除、全选
        self.undoAvailable.connect(self.menu.undo_action.setEnabled)
        self.copyAvailable.connect(self.menu.cut_action.setEnabled)
        self.copyAvailable.connect(self.menu.copy_action.setEnabled)
        self.clipbaord_.dataChanged.connect(self.reset_paste_state)
        self.copyAvailable.connect(self.menu.delete_action.setEnabled)
        self.hasText.connect(self.menu.select_all.setEnabled)
    
    def reset_paste_state(self):
        """重新设置粘贴状态"""
        clipbaord_text = self.clipbaord_.text()
        if clipbaord_text:
            self.menu.paste_action.setEnabled(True)
        else:
            self.menu.paste_action.setEnabled(False)

notepad_main.py不做修改

10.3.2 行为触发

行为触发分析

撤销、剪切、复制、粘贴、删除、全选对应的自带方法如下:

  • 撤销: QPlainTextEdit.undo()
  • 剪切: QPlainTextEdit.cut()
  • 复制: QPlainTextEdit.copy()
  • 粘贴: QPlainTextEdit.paste()
  • 删除: QPlainTextEdit.clear()
  • 全选: QPlainTextEdit.selectAll()

注意: 这也解释了为什么菜单命名,使用的是有_action后缀的,不然的话容易混淆

使用Bing搜索如何实现?

  1. 使用默认浏览器访问网址
    这里需要引入另一个知识QDesktopServices提供了访问常见桌面服务的方法,比如我们要用到的openUrl()方法(openUrl()方法传入一个网页链接然后调用系统默认浏览器打开该链接。)
  2. 使用Bing搜索引擎
    使用https://cn.bing.com/search?q=%s链接并传入搜索内容(%s)
  3. 怎么搜索选中文本?
    a. 获取选中文本?QPlainTextEdit.textCursor().selectedText()可以获取选中文本
    b. 如何将文本传入Url? f-str格式化字符串
  4. 信号触发后直接连接bing_search槽函数即可

代码实现

   def bing_search(self):
        """使用Bing搜索"""
        search_text = self.textCursor().selectedText()
        QDesktopServices.openUrl(f"https://cn.bing.com/search?q={search_text}")      
行为触发代码

custome_content_menu.py 修改如下:

## setup_ui 设置界面里
# 原 self.addAction("使用Bing搜索(&B)")
self.bing_search_action = self.addAction("使用Bing搜索(&B)") # 修改后

custome_plaintext_edit.py修改如下:

## set_event_bind 设置事件绑定里 最下边 添加
self.menu.bing_search_action.triggered.connect(self.bing_search)

## 添加一个类方法
    def bing_search(self):
        """Bing搜索"""
        search_text = self.textCursor().selectedText()
        QDesktopServices.openUrl(f"https://cn.bing.com/search?q={search_text}")     

notepad_main.py不做修改

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐