将坐标文本超出范围的“显示”在QLineEdit上

本文记录了在PyQt中使用QPainter在QLineEdit上绘制文本坐标的实现过程,包括坐标转换、文本居中、反走样等问题的解决,以及QPainter在组件间绘图的限制。作者通过不断尝试和学习,最终找到了在QLineEdit上方创建透明可编辑的顶层行编辑框来显示坐标的方法。
摘要由CSDN通过智能技术生成


前言

先看效果图
最终效果
本文主要记录我实现这个功能的思路历程。实际上这是一个对图片坐标进行方便获取的一个软件其中的坐标显示功能
因本人对 pyqt 的绘画机制不熟悉导致采用了多种后来发现不可行的方法,最终实现此效果也是快乐的学到了很多

注意这是个踩坑记录帖,所以会有很多可能与文章主旨无关的语段

一、可能会被遮挡的简单绘画思路

1. 以程序主窗体作为画板

修改QLineEdit组件这步是不可取的。因为QLineEdit基于QWidget,而这个QLineEdit绝大多数情况下都会在QLayout里面包裹着,我这里用的是一个QGridLayout,那么这个组件怎么改变大小都会影响到他旁边的组件的,最直白的解释就是整个QLayout会被水平扩大。
所以我们需要一种不会影响其他组件的绘画方式:

QPainter.drawText(p:PyQt5.QtCore.QPointF, s:str)

1.1 获取第一参数 绘制文本的坐标

思路很简单,取QLineEdit的pos()就行了嘛。
但是
我试了一下发现不行啊

    def paintEvent(self, event):
        lEFontPointSize = self.lineEdit.font().pointSize()
		# 字体大小统一一下		
        thePos = self.lineEdit.pos()
        qP = QPainter(self)
        qPFont = qP.font()
        qPFont.setPointSize(lEFontPointSize)
        qP.setFont(qPFont)
        qP.drawText(thePos, "干得好小韩,我要给你加工资")

离谱的结果

注意:本文所有的self.lineEdit都是指的这个红箭头右边的行编辑框

离谱的结果,查了一下文档说QWidgets.QWidget.pos()获取到的是这个组件在父组件上的位置。
哦 哦 噢~ 我明白了。我的这个 线体范围 编辑框的父组件是一个QGroupBox组件(当然,这个组上面还有一个组也就是这个编辑框算上窗体总共有三个父组件),所以获取到的pos不是我要的结果。
那就不管它有几个父类了,直接获取以左上角为Form(窗体)原点(0,0)的组件坐标

currentineEditGlobalPos = self.lineEdit.mapTo(self,QPoint(0,0)

方法链接:https://doc.qt.io/qtforpython-5/PySide2/QtWidgets/QWidget.html#PySide2.QtWidgets.PySide2.QtWidgets.QWidget.mapTo
还是说明一下的好,第一参self是我程序的QMainWindow
然后结果是这样的

        currentLineEditGlobalPos = self.lineEdit.mapTo(self,QPoint(0,0))
        qP = QPainter(self)

        qPFont = qP.font()
        lEFontPointSize = self.lineEdit.font().pointSize()
        qPFont.setPointSize(lEFontPointSize)
        qP.setFont(qPFont)
        
        qP.drawText(currentLineEditGlobalPos, "0,64-12,78-45,99")
        return super().paintEvent(event)

文字图形有偏移
这又使我开始怀疑了这个坐标真的正确吗? 二话不说 我给写了一段绘制行编辑框的 边框 以作判断

    def paintEvent(self, event):
        lineEditRect = self.lineEdit.geometry()
        currentLineEditGlobalPos = self.lineEdit.mapTo(self,QPoint(0,0))
        lineEditRect.moveTo(currentLineEditGlobalPos)
        qP = QPainter(self)
        qP.setPen(QColor(Qt.red))

        qPFont = qP.font()
        lEFontPointSize = self.lineEdit.font().pointSize()
        qPFont.setPointSize(lEFontPointSize)
        qP.setFont(qPFont)

        qP.drawText(currentLineEditGlobalPos, "0,64-12,78-45,99")
        qP.drawRect(lineEditRect)
        return super().paintEvent(event)

完犊子子
完了,一波未平一波又浪起?!
额,通过上图可以看出

  1. 这个QPainter.drawText(p:PyQt5.QtCore.QPointF, s:str)会以传入的第一参作为绘制图形的左下角???而不是一般的QPoint(0,0)为左上角原点的逻辑
  2. 这个绘制出来的矩形右面和下面都多出来一个像素

emmmm 再开两个小标题吧

1.1.1 大浪一 如何对齐文本?

双仔细查了一下文档 发现这个过载函数,如果前面的参数是让给一个矩形的话那 y 値就是所要绘制文本的顶部,而如果前面的参数是一个坐标的话那y 値就是所要绘制文本的基线,放三个例子

QPainter.drawText(x:int, y:int, w:int, h:int, flags:int, text:str)
QPainter.drawText(r:QtCore.QRectF, text:str[, o=QTextOption()])
QPainter.drawText(p:QPoint, s:str)

y値是绘制文本的顶部可以理解,可y値作为所要绘制文本的 基线 是个什么意思?
在我和百度一顿疯狂合作过后 我查到了下面这张图
不明觉厉
图片引用于:https://blog.csdn.net/TemetNosce/article/details/78068520
发现这东西大多都是和CSS有关的教程
然后按照大佬的解释我写出以下代码

    def paintEvent(self, event):
        self.lineEdit.setStyleSheet("background-color: rgba(0, 0, 0, 0);")
        qP = QPainter(self)

        # 文本垂直居中
        qPMetrics = qP.fontMetrics()  # 取出painter目前的字体规格
        # qt的fontMetrics不提供图中的Lineheight 所以自己累加一个
        lineHeight = qPMetrics.ascent() + qPMetrics.descent() + qPMetrics.leading()
        fontMetricsCenterLine = lineHeight / 2  # 计算字体中线
        # 计算 字体规格中线 到 基线 的距离
        distanceFromCenterLineToBaseLine = fontMetricsCenterLine - (qPMetrics.descent() + qPMetrics.leading())
        # 将 行编辑框的中线 加上 distanceFromCenterLineToBaseLine
        # 这样 不管行编辑框有多 高(rect().height()) 都能将文本绘制在行编辑框中心
        lineEditFontBaseLine = self.lineEdit.rect().height() / 2 + distanceFromCenterLineToBaseLine

        currentLineEditGlobalPos = self.lineEdit.mapTo(self, QPoint(0, 0))
        currentLineEditGlobalPos.setY(currentLineEditGlobalPos.y() + lineEditFontBaseLine)

        qPFont = qP.font()
        lEFontPointSize = self.lineEdit.font().pointSize()
        qPFont.setPointSize(lEFontPointSize)
        qP.setFont(qPFont)
        qP.drawText(currentLineEditGlobalPos, "0,64-12,78-45,99")

        return super().paintEvent(event)

然后我们就得到了一个和使用QPainter.drawStaticText() (这个方法的会将第一参数pos作为绘制的左上角)一样的显示效果
直接照抄大佬的肯定没啥意思哈,我给做了一个可以根据矩形框高度变化而始终垂直居中的功能,然而失败了 我的坐标少了一个像素,开了字体抗锯齿也不行 我暂时先放弃了, 哈哈本来想露一手的

另外这个这个文本左边距我实在查不出来,那个黑色的边框是QLineEdit.setFrame(b:bool)设置开启的,可是我在源代码中没有找到这个Frame启用后被绘制的语句啊?
QLineEdit.setFrame()源代码:https://code.woboq.org/qt5/qtbase/src/widgets/widgets/qlineedit.cpp.html#518

1.1.2 大浪二 QPainter的反走样

1.1.2 大浪二一(QRect的历史遗留问题)
擦 图片不放大都可以看出行编辑框右方和下方溢出来了
emmm 那我把x轴偏移一下 看看左上角是否正常
x轴向左偏移10个像素

倒立思考。。。。。
再看文档,发现了坐标系统介绍了这种情况
坐标系统:https://doc.qt.io/qtforpython-5/overviews/coordsys.html#aliased-painting
QPainter不开抗锯齿是上面那种情况
开了抗锯齿之后是这样的

painter.setRenderHint(QPainter.Antialiasing, True)

开了抗锯齿后的矩形效果
我还以为是QRect历史遗留的right()和bottom()无法获取到真实坐标的问题呢
坐标系文档:https://doc.qt.io/qtforpython-5/overviews/coordsys.html#coordinate-system

1.2 获取第二参数 要绘制的文本

因为显示的这串文本使用到了QPolygon,所以要记录一下这个QPolygon所存在的一个我认为的奇葩的问题。
首先我需要先对这个QPolygon进行内容处理,然后使用到了一个名为 push_back()的方法,最后解释器当场给了我一个大比子
魔力红

我迅速检查了一下

我是用的Python是由Anaconda安装为默认的系统python解释器

Python 3.8.12 (default, Oct 12 2021, 03:01:40) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32

以及PyQt5的信息

Name: PyQt5
Version: 5.15.4
Summary: Python bindings for the Qt cross platform application toolkit
Home-page: https://www.riverbankcomputing.com/software/pyqt/
Author: Riverbank Computing Limited
Author-email: info@riverbankcomputing.com
License: GPL v3
Location: c:\users\gaojie\miniconda3\lib\site-packages
Requires: PyQt5-Qt5, PyQt5-sip
Required-by: pyqt5-tools, pyqt5-plugins

最后是我参阅的文档是
文档
? 我这 pyqt 环境也就修正版本号大了2也不至于是这样的啊
当然也有朋友一眼就看出这个其实是pyside2的手册,但都是基于Qt5.12的啊
没理由啊
我又查了一下安装在我本地的QPolygonF.py文件的结构,果然有问题 他和手册上的方法数量是对不上的
方法名对比
文本对比网站链接: http://www.jsons.cn/txtdiff/
害 也没必要换版本了,手动来吧

2. 以QLineEdit作为画板(QPainter的警告)

其实可以注意一下QPainter的坐标原点的,把它绘制的目标改为QLineEdit本身不就不需要什么mapTo的了吗
但是
我试了一下发现不行啊
木有反应,所以就没必要上图了,我叒进行了一顿文档浏览,终于发现了

Warning
When the paintdevice is a widget, QPainter can only be used inside a paintEvent() function or in a function called by paintEvent().

警告 当绘画设备1是一个组件时,QPainter只能用于当前组件的paintEvent()循环事件方法中,也可以是paintEvent()调用的函数。
我回想了一下疏忽原因(狡辩),因大多数网络教程中(我学的书中也是)都是举得画在窗体(QWdiget,QMainWindow,QDialog)上的例子,所以没有说明这个警告
那我的情况是想要在QMainWindow的paintEvent()中使用QPainter来绘制QLineEdit这个组件,这是不行的。
悬空倒立思考。。。。。
哦 哦 噢~ 我明白了。方法有两种,可以继承QLineEdit这个类,做些简单修改以支持这个功能。或者使用书中提到过的一个叫做eventFilter的方法

    def __init__(self):

        .....

        # 设置行编辑框的背景为透明的,不然画出来的文本会被遮挡
        self.lineEdit.setStyleSheet("background-color: rgba(0, 0, 0, 0);")
		# 将行编辑框的事件注册给主窗体的事件过滤器
        self.lineEdit.installEventFilter(self)   	  

    def eventFilter(self, 被监视对象, 被监视对象的事件):
        if 被监视对象 == self.lineEdit:
            if 被监视对象的事件.type() == QEvent.Paint:
                qP = QPainter(被监视对象)
                qP.drawText(QPoint(0, 10), contentText)
                # 注意 这个y轴的10,也是上面说到的 drawText会以传入的pos作为绘制的左下角
        return super().eventFilter(被监视对象, 被监视对象的事件)

继承自定义类就不举例了,吃力不讨好也没eventFilter优雅。
那么最终结果就是,这个QPainter所显示的效果和QLineEdit的显示效果是一样的,文字都被限制在了QLineEdit的Rect中,很容易就明白了 QPainter.begin(self.lineEdit) 那这个画板就是 行编辑框这么大的Rect了。害

3. 这个方法不可行

把图形画在主窗体2上对于我的这个程序并不可行
一、1.1 章节中 第一个图片中的文字没被遮挡,是因为我设置了组件背景透明
A:我以为会有办法能解决这个问题的,我查了许多资料(百度,谷歌,文档),都不太行,虽然想起了QGraphicsView中有一个降低和提高图形项目的方法 然后QWIdget中也有QtWidgets.QWidget.raise_()、QtWidgets.QWidget.lower()和QtWidgets.QWidget.stackUnder(),但是把组件降低到主窗体2下面岂不是更麻烦,更加无厘头。


二、不会被遮挡的简单绘画思路

1. 以最高层的Widget作为画板

其实就是在主窗体2上添加一个QWidget,有以下几点需要注意下

  1. 因为这个QWidget是最后绘制的 所以它肯定在所有组件上面,然后再使用QPainter在这个QWidget上面绘制图形就肯定不会再被遮挡了
  2. 这个QWidget是要替代原本在所有组件最底层的主窗体2,所以需要让这个QWidget的 “Rect” 是相同的,不然绘画坐标原点不同就不能用了。
  3. 在一、2.小节中说过,像这种在主窗体 2 中使用QPainter对其他QWidget进行绘制的操作,需要使用eventFilter进行捕捉过滤才可以。
        self._drawingBoardWidget = QWidget(self)
        self._drawingBoardWidget.setObjectName("drawingBoardWidget")
        self._drawingBoardWidget.installEventFilter(self)

    def eventFilter(self, watched, event):
        if watched == self._drawingBoardWidget:
            if event.type() == QEvent.Paint:
                current_aLR_LE_GlobalPos = self.lineEdit.mapTo(self, QPoint(0, 0))
                current_aLR_LE_GlobalPos.setX(current_aLR_LE_GlobalPos.x() + 3)
                current_aLR_LE_GlobalPos.setY(current_aLR_LE_GlobalPos.y() + 14)
                qP = QPainter()
                qP.setRenderHint(QPainter.TextAntialiasing)
                qP.begin(watched)
                qPFont = qP.font()
                qPFont.setPointSize(self.lineEdit.font().pointSize())
                qP.setFont(qPFont)
                qP.drawText(current_aLR_LE_GlobalPos, "0,64-12,78-45,99")
                qP.end()
        return super().eventFilter(watched, event)

代码显示效果
看着还不错满足的基本要求,只不过妥协后的坐标调整看着太膈应人了

2. 直接使用现成的QLineEdit

Q:与其添加一个顶层QWidget作为QPainter的绘画板 那为什么不直接添加一个QLineEdit 然后做成透明 再加一个鼠标穿透呢?
A:因为我刚想到XD。。。。。。。

这个方法更加方便优雅,同时也解决了文本居中的问题

	def __init__(self):
		self.initTopLineEdit()

	def initTopLineEdit(self):
        self.topLineEdit = QLineEdit(self)
        self.topLineEdit.setObjectName("topLineEdit")
        self.topLineEdit.setText("0,64-12,78-45,99-121,999-0,64-12,78-45,99-121,999")
        # 设置鼠标穿透
        self.topLineEdit.setAttribute(Qt.WA_TransparentForMouseEvents)
        # 设置边框、背景颜色为透明色
        self.topLineEdit.setStyleSheet(
            "border-width:1px;border-style:solid;border-color: rgba(0, 0, 0, 0);background-color: rgba(0, 0, 0, 0);")
            
    def showEvent(self, event):
        currentLineEditGlobalPos = self.lineEdit.mapTo(self, QPoint(0, 0))

        # 设置同等高度
        lineEditGeometry = self.lineEdit.geometry()
        self.topLineEdit.setFixedHeight(lineEditGeometry.height())
        # 设置宽度为从组件现在的位置到窗体的最右边
        mainwindowGeometry = self.geometry()
        mainWindowWidth = mainwindowGeometry.width() - currentLineEditGlobalPos.x()
        self.topLineEdit.setFixedWidth(mainWindowWidth)
	    # 将topLineEdit覆盖self.lineEdit
        self.topLineEdit.move(currentLineEditGlobalPos)

        return super().showEvent(event)

这个移动操作放在showEvent中有讲究的,因为只有窗体被

form.show()

之后才能获取到这个self.lineEdit的坐标,所以直接把这些代码写在showEvent事件中是最方便的
不然每次继承这个类之后都要这样做

form.show()
form.moveTopLineEdit()		

如果有机会我会再优化一下操作逻辑,在只有点击黑框中的区域时可以将编辑框中的内容设置到系统剪切板中
优化
也就是self.lineEdit.rect()的范围

3. 可操作的顶层行编辑框

哈哈,时隔多日 机会来了。这个小章节就是为了解决上面那个问题的。
我再重新介绍下:现在的界面是 本来UI上面就有一个行编辑框(UnderLineEdit) 然后我又做了一个自定义的置顶行编辑框(先叫做TopLineEdit)覆盖在上面了,TopLineEdit是用来展示数据的目前不可操作(修改,复制等),因为我给它开了鼠标穿透。UnderLineEdit是给TopLineEdit提供坐标,大小以及其他状态的同步的,现在的问题是要解决怎样做到可修改TopLineEdit又不会影响到右边的GraphicsView的相关操作呢?
答:这个问题也是想了挺久的,以前稍微入门了一点qml其中发现qml特有的MouseArea貌似很类似我这个要求,不过为了这么个功能给主窗体换成qml的代价实在太大了,当然我知道QWidget可以嵌入QtQuickWidget,这个领域不是太清楚就暂时搁置了。后来又想到以前看到过不规则窗体的代码实现 文章,也就是setMask,但是仔细研究下来发现这不是同一种情况,最后 我就像喜羊羊先这样
这样
再那样
那样
最后不倒立思考了哈
亮灯泡

嘿嘿,最后的完美解决方法是 把UnderLineEdit的鼠标事件注册给TopLineEdit,然后TopLineEdit在eventFilter中检测到UnderLinEdit的鼠标事件被触发就把当前的输入焦点设置为TopLineEdit,呐 代码长这个样

	self.filterEvents = [QEvent.MouseButtonDblClick, QEvent.MouseButtonPress, QEvent.MouseButtonRelease]

    def eventFilter(self, watched, event):
        if watched == self.underLineEditObject:
            if event.type() in self.filterEvents:
                self.underLineEditObject.clear()
                self.setFocus()	# 这个self就是TopLineEdit
        return super().eventFilter(watched, event)

这样我们就达到了当鼠标按键在红色矩形区域内 双击,按下,松开就可以修改TopLineEdit中的数值了
而黄色矩形框圈到的虽然也是TopLineEdit的内容,但是对这个区域内进行以上鼠标操作是不会有任何反应及影响的,
额。。应该也能说 在这个黄色矩形区域内鼠标穿透依旧生效。
完美
其实就是一状态传递 嘿


总结

写这篇文章收获颇多,写的时候对这些知识做一个巩固,又做了更进一步的思考,文章内容不断修改,标题层级不断划分,最终也是得到了顶层透明QLineEdit的写法(写法虽然没什么技术含量,但信息差往往是小白最难得到的)。
最后
真棒啊!


  1. (paintdevice是指QPainter.begin的参数) ↩︎

  2. 指程序所使用的QMainWindow、QDialog和QWidget其中之一作为主窗体(窗口) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值