
在日常的图像处理、工程测量或学习工作中,我们常常需要快速测量图片中某个夹角的大小。虽然市面上已有许多专业软件,但若能亲手打造一个轻量、可定制、界面美观的量角器工具,不仅满足个性化需求,更能加深对图形界面编程和几何算法的理解。本文将带你使用Python和PyQt5从零构建一个功能完备的量角器,支持导入常见格式图片,通过鼠标点击三个点实时测量角度,并绘制直观的辅助线。代码经过多轮优化,解决了光标干扰、角度标记不准等问题,最终呈现一个既专业又易用的工具。技术选型与设计哲学选择PyQt5作为GUI框架,是因为它提供了强大的QGraphicsView/QGraphicsScene架构,非常适合处理图像显示与矢量图形叠加。我们利用QGraphicsPixmapItem显示图片,通过自定义QGraphicsScene子类来管理鼠标事件和图形项(点、线、弧、文本)。整个界面采用深色主题,搭配亮色的绘制元素,确保在各种背景下都有良好的可视性。右侧浮动面板实时显示角度数值,状态栏提供操作提示,工具栏集成常用功能,整体布局紧凑且符合现代软件审美。核心功能实现详解1. 图片加载与视图操控通过QFileDialog选择图片,加载为QPixmap并设置到场景的QGraphicsPixmapItem中。同时设置场景矩形与图片尺寸一致,便于后续视图缩放。视图支持拖拽平移(ScrollHandDrag)和以鼠标为中心的缩放(AnchorUnderMouse),用户可自由浏览图片细节。2. 点选与角度计算用户依次点击三个点,顺序为:角的一边上一点 → 顶点 → 另一边上一点。场景记录这些点的坐标,当点数达到2时绘制第一条边,达到3时计算夹角。角度计算采用向量点积公式:[ \theta = \arccos\left(\frac{\vec{v_1} \cdot \vec{v_2}}{|\vec{v_1}| |\vec{v_2}|}\right) ]得到弧度后转为度数,范围0~180°。为防止浮点误差,对余弦值做了截断处理。3. 图形绘制与实时反馈点:用红色圆点标记,边框为白色,确保醒目。边:用亮蓝色直线连接,线宽2像素,清晰标示角的两边。弧:以顶点为圆心,固定半径绘制金黄色弧线,指示角度范围。弧线绘制使用了QPainterPath的arcTo方法,需注意起始角和跨度的正确计算,并处理零角度和180°的特殊情况。角度文本:在顶点旁动态显示当前角度值(例如“45.2°”),字体加粗、颜色与弧线一致,位置偏移避免遮挡。4. 界面布局与交互优化主窗口包含中央视图区、顶部工具栏、右侧可停靠面板和底部状态栏。工具栏提供打开图片、清除测量、缩放、适应窗口等快捷操作。右侧面板实时更新角度数值,并附有简明操作说明。状态栏在每一步给出提示,降低学习成本。优化点:从“能用”到“好用”在初步实现后,我们针对用户体验做了几项关键优化:光标形态:初始版本默认手形光标,导致点选时不够精准。现改为十字光标(Qt.CrossCursor),拖拽平移时自动变为手形,松开恢复十字,兼顾精准与便捷。角度标记:之前仅在右侧面板显示角度值,用户需来回移动视线。现增加顶点旁的浮动文本,数值与面板同步,一目了然。弧线绘制健壮性:当三点共线(角度0°或180°)时,不再绘制弧线,避免产生混乱的图形。同时优化了弧线方向,确保始终从第一条边转向第二条边,符合直观。清除全面性:确保清除测量时移除所有图形项(点、线、弧、文本),不留残影。代码实现与使用说明以下为完整的量角器程序代码,复制保存为.py文件,安装PyQt5后即可运行。界面友好,操作简单:打开图片后,依次点击三个点,角度自动显示并可重复测量。importsysimportmathfromPyQt5.QtWidgetsimport(QApplication,QMainWindow,QGraphicsView,QGraphicsScene,QGraphicsPixmapItem,QGraphicsTextItem,QVBoxLayout,QWidget,QLabel,QPushButton,QFileDialog,QToolBar,QAction,QStatusBar,QGraphicsEllipseItem,QGraphicsLineItem,QGraphicsPathItem,QDockWidget)fromPyQt5.QtCoreimportQt,QPointF,QRectF,pyqtSignal,QSizefromPyQt5.QtGuiimport(QPixmap,QPen,QBrush,QColor,QPainterPath,QFont,QIcon,QPainter)classImageScene(QGraphicsScene):"""自定义场景,处理鼠标点击并绘制测量图形"""angle_changed=pyqtSignal(float)def__init__(self,parent=None):super().__init__(parent)self.points=[]# 存储三个点self.point_items=[]# 点的图形项self.line_items=[]# 边的图形项self.arc_item=None# 弧线self.angle_text_item=None# 角度文本self.angle_value=0.0# 当前角度self.pixmap_item=None# 图片项self.point_radius=6# 点的半径self.arc_radius=60# 弧线半径self.setBackgroundBrush(QBrush(QColor(45,45,45)))defset_pixmap(self,pixmap):"""设置图片并清空之前的测量"""self.clear_measurement()ifself.pixmap_item:self.removeItem(self.pixmap_item)self.pixmap_item=QGraphicsPixmapItem(pixmap)self.addItem(self.pixmap_item)self.setSceneRect(QRectF(pixmap.rect()))defclear_measurement(self):"""清除所有测量图形和点"""self.points.clear()foriteminself.point_items:self.removeItem(item)self.point_items.clear()foriteminself.line_items:self.removeItem(item)self.line_items.clear()ifself.arc_item:self.removeItem(self.arc_item)self.arc_item=Noneifself.angle_text_item:self.removeItem(self.angle_text_item)self.angle_text_item=Noneself.angle_value=0.0self.angle_changed.emit(0.0)defadd_point(self,pos):"""添加一个新点(超过三个则自动清空)"""ifself.pixmap_itemandnotself.pixmap_item.contains(pos):returniflen(self.points)=3:self.clear_measurement()self.points.append(pos)self._dra