来源
最初的想法来自 计算机图形学的课程项目作业,其要求基于 Python + Qt 实现一个支持简单图元绘制的 GUI+CLI 程序。
之前一般仅使用 Python 写一些简单的小型项目。曾写过一个较大项目,但即使使用了类型标注,也很快随着项目复杂度的提升,遇到了维护瓶颈,大大影响开发效率,遂中断了开发。
这次是第一次使用 Python 开发图形用户界面的软件,也是第一次使用 Qt,考虑到这个项目的需求较为明确并且有足够的扩展可能,决定将此项目作为一个 Python 较大单人项目的练手,并规避以前 Python 项目遇到的一些问题。
Idea
根据课程项目的模板代码,发现此项目非常适合 OOP 模式设计。结合以前的 WPF 学习经历,决定依照 WPF 的设计思路,试图在 ImagingS 中复刻 WPF 的呈现模型。
- 图元 Geometry 对象描述图元及其绘制算法
- 绘图 Drawing 将各种图元绘制到 DrawingContext 上
- 绘图上下文 DrawingContext 提供具体绘图的抽象,统一 GUI 绘图和图片文件绘图。
通过这三个层次,将呈现系统的两部分:定义与呈现分离。
设计
核心的 API 集中在以下几个类中,其中与绘制抽象相关的大部分类均能在 WPF 绘制模型中找到对应。
- 图元类:各种图元的基类,定义了绘制算法(strokePoints)和变换。
class Geometry(PropertySerializable, ABC):
def __init__(self) -> None: pass
def transform(self) -> Optional[Transform]: pass
def strokePoints(self, pen: Pen) -> Iterable[Point]: pass
def fillPoints(self) -> Iterable[Point]: pass
def inStroke(self, pen: Pen, point: Point) -> bool: pass
def inFill(self, point: Point) -> bool: pass
def transformed(self) -> Geometry: pass
def bounds(self) -> Rect: pass
- 绘制类:所有可绘制元素的基类,定义了绘制函数(render)。对于图元对象,实现了 GeometryDrawing 来实际完成图元的绘制任务。
class Drawing(PropertySerializable, IdObject, ABC):
def __init__(self) -> None: pass
@abstractmethod
def render(self, context: RenderContext) -> None: pass
@property
@abstractmethod
def bounds(self) -> Rect: pass
- 绘图上下文类:对实际绘制的目标的抽象
class RenderContext(ABC):
@abstractmethod
def _point(self, position: Point, color: Color) -> None: pass
@abstractmethod
def bounds(self) -> Rect: pass
def point(self, position: Point, color: Color) -> None: pass
def points(self, positions: Iterable[Point], brush: Brush)
-> None: pass
- 变换类:所有变换的基类,定义了如何将一个点变换到另一个点
class Transform(PropertySerializable, IdObject, ABC):
def __init__(self) -> None: pass
@abstractmethod
def transform(self, origin: Point) -> Point: pass
- 文档类:定义了当前文档所包含的画刷,图元,画布大小等信息,实现了序列化操作
class Document(PropertySerializable, IdObject):
def __init__(self) -> None: pass
@property
def brushes(self) -> IdObjectList[Brush]: pass
@property
def drawings(self) -> DrawingGroup: pass
@property
def size(self) -> Size: pass
def save(self, file, format: DocumentFormat = DocumentFormat.ISD)
-> None: pass
@staticmethod
def load(file, format: DocumentFormat = DocumentFormat.ISD)
-> Document: pass
- 画布类:将抽象绘制对象包装成 Qt 中的绘制对象
class Canvas(QGraphicsView): pass
class DrawingItem(QGraphicsItem):
def __init__(self, drawing: Drawing,
size: QSizeF, parent: Optional[QGraphicsItem] = None): pass
- 交互类:定义了 GUI 上与图元交互的所有操作,提供给画布一个统一的接口来处理用户交互
class Interactivity(QObject):
started = pyqtSignal(QObject)
ended = pyqtSignal(QObject)
updated = pyqtSignal(QObject)
def __init__(self) -> None: pass
def start(self) -> None: pass
def end(self, success: bool) -> None: pass
def update(self) -> None: pass
@property
def viewItem(self) -> Optional[QGraphicsItem]: pass
@property
def state(self) -> InteractivityState: pass
def onMousePress(self, point: QPointF) -> None: pass
def onMouseMove(self, point: QPointF) -> None: pass
def onMouseRelease(self, point: QPointF) -> None: pass
def onMouseDoubleClick(self, point: QPointF) -> None: pass
def onKeyPress(self, key: QKeyEvent) -> None: pass
def onKeyRelease(self, key: QKeyEvent) -> None: pass
实现
- To be done
总结
这个项目借鉴了 WPF 呈现模型的设计,一个成熟的模型的确是富有扩展性和较易维护的。Python + Qt 的组合开发效率也是很高的,但是运行效率有一定损失。
项目目前的主要不足,一是图形学相关算法实现较少;二是部分类的 API 设计不够准确,这部分可能来源于 Qt 绘图模型和 WPF 依赖的 DirectX 两者的不一致,复刻的过程中部分设计需要适配 Qt ,造成一定妥协(例如 PyQt 的绘制效率低下,导致部分功能不容易在此模型下高效实现)。