炮打 EasyX

TL;DR:EasyX 就是纯纯的垃圾,别用。入门建议用 SFML。

背景

之前一直没写什么东西,现在期末考完没啥事情正好写一点。

这个学期软院开了个什么”信息技术基础认知与实践“的课,就是期末提出来那么两周,每周两次,每次四个课时给你写代码,要求最后一个人能写个小项目。我选的是 C++ 方向。然后我把之前选课指导里面写的东西贴在这里:

主要内容包括面向对象设计模式、操作系统与应用程序、句柄与回调函数设计、消息映射、软件项目管理、游戏设计基础、图形动画处理等。要求学生掌握面向对象设计模试;掌握操作系统与应用程序关系;了解程序与柄关系;掌握软件产生与周期;掌握游戏开发流程,场景;掌握常用图形处理API;并通过完整的项目案例分析和成果展示,使学生掌握桌面应用项目开发的流程,培养团队协作的能力。其中,面向对象设计模式和游戏设计基础是学习重点,操作系统与应用程序和软件项目管理是难点。

好吧……其实这个地方是画了很大个大饼。首先之前就没给人讲过的环境配置这次还是没讲清楚,构建系统仍然是讲都不讲,最后不知道从哪里掏出来个 VC++6 我一口老血差点没吐出来,然后上面的人在那讲,下面的人傻呵呵的跟着做,不是你们都不觉得哪里有问题吗?然后版本控制也不介绍。最后,四四十六个课时,后八个课时讲了一点面向对象和项目组织,有节课 lecturer 嗯是被一个简单的循环依赖问题硬控整整一个半小时;前八个课时全在讲 EasyX,我们今天的主角。

炮打 EasyX:作为垃圾的入门框架

优点?为什么要谈优点?

as if 我只要谈了优点,就有什么人就会连连说,萌哥中肯,分析井井有条还不失客观性一样,或者 EasyX 的孝子贤孙们(存在这种人吗?)看到了就不会对我有意见一样。

显然,我是来批判的,不是来装中肯的。虚情假意写一点优点不如不写。

主要针对 EasyX 及其作者个人存在的以下几个重大问题(甚至毫不客气地说,重大弊病)进行讨论。

  1. API 严重的平台特定性

  2. 糟糕的设计和令人不明所以的文档

  3. 作者本人令人迷惑的运营能力

严重的平台特定性问题

这个东西真的是一看便知。首先让我们看到 easyx.h

1
#include <windows.h>

I mean, what the hell?

一上来就给人干绷不住了。如果 EasyX 只是个学生作业我觉得无可厚非,但是 EasyX 在事实上已经一个被很多机构甚至是部分高校用作教学的得到广泛应用的项目了,然而这么一个项目却根本没法覆盖到全部场景——你凭什么假定我就使用的是 Windows?这时候有人就要说了,你装个 MinGW 工具链不就好了。没错,但是作为一个大项目就要有大项目的担当。一方面标榜着 Easy,一方面又要让刚刚入门的用户去事必躬亲地处理各种细节问题,我认为这么做并不合适。

EasyX 事实上仅仅是在 GDI 外面套了薄薄的一层壳子,封装了约等于没有封装,简化了约等于没简化,那么我就想问了,用它的意义何在,为什么我不直接去调 GDI。

垃圾的设计、垃圾的文档

首先,命名极其随意。

1
2
3
4
5
6
COLORREF getlinecolor();            // Get line color
void setlinecolor(COLORREF color); // Set line color
COLORREF gettextcolor(); // Get text color
void settextcolor(COLORREF color); // Set text color
COLORREF getfillcolor(); // Get fill color
void setfillcolor(COLORREF color); // Set fill color

好的,我们注意到作者同志似乎不太喜欢断句。这种非 camel case 非 snake case 的颇具淳古之风的没有脑血栓想不出来的命名法实在是令人大为震撼。不过至少还算中立邪恶,没有用拼音或者谜之缩写……

没有吗?

1
2
COLORREF getbkcolor();                // Get background color
void setbkcolor(COLORREF color); // Set background color

当我没说。好的我又想问了,作者同志是真的惜字如金啊,一个 background 嗯是给他缩写成 bk 了。好好写完一个词很难吗。有没有一种可能现在大家写代码都有提示了,多写点少写点其实没有什么区别。如果作者同志真的想要整点仿古的北欧性冷淡极简风代码给我这种土鳖开开眼,为什么不缩写成 C 标准库里面那种 6 字神人函数名。

1
2
void settxc(...);
void setbkc(...);

可能我确实有点极端了,至少人家命名风格还是相对统一的……

是这样吗?

1
2
3
4
5
void BeginBatchDraw();    // Begin batch drawing mode
void FlushBatchDraw(); // Refreshes the undisplayed drawing
void FlushBatchDraw(int left, int top, int right, int bottom); // Refreshes the undisplayed drawing
void EndBatchDraw(); // End batch drawing mode and refreshes the undisplayed drawing
void EndBatchDraw(int left, int top, int right, int bottom); // End batch drawing mode and refreshes the undisplayed drawing

哦怎么又变成 big camel case 了。作者同志终于想起来世界上居然还有断句这种东西了。

1
bool InputBox(LPTSTR pString, int nMaxCount, LPCTSTR pPrompt = NULL, LPCTSTR pTitle = NULL, LPCTSTR pDefault = NULL, int width = 0, int height = 0, bool bOnlyOK = true);

然后参数也从之前的乱来风格(一会匈牙利一会作者最爱的不断句魔改版匈牙利)改成匈牙利了。不禁令人好奇他到底想要干什么。

有人说,这不是尬黑吗?至少人家一个系列的 API ——一个 API 族——内部风格是统一的!你这属于是找些莫名其妙的借口抹黑我们 EasyX。还给你搞上 family 了!咱们 Vulkan 分功能有 Queue family,EasyX 分功能有 API family,一个 family 里面的东西才是 consistent 的,不是一个 family 就不保证各方面的 consistency,非常高级,接轨国际!好嘛……

1
2
3
4
5
6
7
8
9
10
11
12
13
// Image related functions
void loadimage(IMAGE *pDstImg, LPCTSTR pImgFile, int nWidth = 0, int nHeight = 0, bool bResize = false); // Load image from a file (bmp/gif/jpg/png/tif/emf/wmf/ico)
void loadimage(IMAGE *pDstImg, LPCTSTR pResType, LPCTSTR pResName, int nWidth = 0, int nHeight = 0, bool bResize = false); // Load image from resources (bmp/gif/jpg/png/tif/emf/wmf/ico)
void saveimage(LPCTSTR pImgFile, IMAGE* pImg = NULL); // Save image to a file (bmp/gif/jpg/png/tif)
void getimage(IMAGE *pDstImg, int srcX, int srcY, int srcWidth, int srcHeight); // Get image from device
void putimage(int dstX, int dstY, const IMAGE *pSrcImg, DWORD dwRop = SRCCOPY); // Put image to device
void putimage(int dstX, int dstY, int dstWidth, int dstHeight, const IMAGE *pSrcImg, int srcX, int srcY, DWORD dwRop = SRCCOPY); // Put image to device
void rotateimage(IMAGE *dstimg, IMAGE *srcimg, double radian, COLORREF bkcolor = BLACK, bool autosize = false, bool highquality = true);// Rotate image
void Resize(IMAGE* pImg, int width, int height); // Resize the device
DWORD* GetImageBuffer(IMAGE* pImg = NULL); // Get the display buffer of the graphics device
IMAGE* GetWorkingImage(); // Get current graphics device
void SetWorkingImage(IMAGE* pImg = NULL); // Set current graphics device
HDC GetImageHDC(IMAGE* pImg = NULL); // Get the graphics device handle

请问这是什么意思?

一个图形变换能给我整出两套不一样的命名法来,这个作者确实是有水平的。他自己写下面那行的时候能不能看一下自己之前写的啥?

然后有人又要说了,EasyX 是大项目,要保证 API 的前向兼容性……你能不能把旧的函数用宏或者随便什么东西设成新函数的别名,然后加一个 deprecation warning,相信 EasyX 的作者是做了这个功能的了吧。

1
2
3
4
5
6
7
8
9
#if _MSC_VER > 1200 && _MSC_VER < 1900
#define _EASYX_DEPRECATE __declspec(deprecated("This function is deprecated."))
#define _EASYX_DEPRECATE_WITHNEW(_NewFunc) __declspec(deprecated("This function is deprecated. Instead, use this new function: " #_NewFunc ". See https://docs.easyx.cn/" #_NewFunc " for details."))
#define _EASYX_DEPRECATE_OVERLOAD(_Func) __declspec(deprecated("This overload is deprecated. See https://docs.easyx.cn/" #_Func " for details."))
#else
#define _EASYX_DEPRECATE
#define _EASYX_DEPRECATE_WITHNEW(_NewFunc)
#define _EASYX_DEPRECATE_OVERLOAD(_Func)
#endif

好消息,做了;坏消息,这个功能是 MSVC exclusive 的。

但是众所周知有一个东西叫做 [[deprecated]] 注解。莫非你 EasyX 用的就是个古董 C++ 然后一点现代特性都没有吗?那你用 C++ 的意义在哪里啊?

1
2
3
#ifndef __cplusplus
#error EasyX is only for C++
#endif

哦,原来用了类啊。但是对那几个类的操作为什么不是调用方法而是给某些函数传指针啊?我到底写的是 C++ 还是 C 啊我请问了?为什么不直接用 C 写呢?

再来看看这个。

1
2
3
4
int  getrop2();                        // Get binary raster operation mode
void setrop2(int mode); // Set binary raster operation mode
int getpolyfillmode(); // Get polygon fill mode
void setpolyfillmode(int mode); // Set polygon fill mode

当时我和我室友看着这几个 modes out of nowhere 感到无比的迷惑。然后我才反应过来这几个地方是要填 GDI 里面那些常量的。比如前两个就是选择黑/白/覆盖/按位和/按位异或/按位取反那些的,其实就是封装的 GetROP2SetROP2 两个函数。

把这里要填哪些值、能填哪些值写出来真的很难吗?

写个意思清晰点的类型别名很难吗?

写一个案例,比如

1
2
3
4
5
6
7
8
9
10
11
12
// Raster operation mode.
// ...
// R2_BLACK : ...
// R2_COPYPEN : ...
// ...
// See also: https://learn.microsoft.com/...
typedef int RasterOperationMode;

// Get binary raster operation mode
RasterOperationMode getrop2();
// Set binary raster operation mode
void setrop2(RasterOperationMode nMode);

其实我觉得他要真用 C++ 就应该把这些玩意都给包装成 enum class,或者至少包装成个 enum 然后内部去处理 GDI 那些事情。然后他偏不,非要把这些玩意统统给你用户处理,至于用户怎么难受就不是他的问题了。

另外一个典型例子是这个消息封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Message Structure
struct ExMessage
{
USHORT message; // The message identifier
union
{
// Data of the mouse message
struct
{
bool ctrl :1; // Indicates whether the CTRL key is pressed
bool shift :1; // Indicates whether the SHIFT key is pressed
bool lbutton :1; // Indicates whether the left mouse button is pressed
bool mbutton :1; // Indicates whether the middle mouse button is pressed
bool rbutton :1; // Indicates whether the right mouse button is pressed
short x; // The x-coordinate of the cursor
short y; // The y-coordinate of the cursor
short wheel; // The distance the wheel is rotated, expressed in multiples or divisions of 120
};

// Data of the key message
struct
{
BYTE vkcode; // The virtual-key code of the key
BYTE scancode; // The scan code of the key. The value depends on the OEM
bool extended :1; // Indicates whether the key is an extended key, such as a function key or a key on the numeric keypad. The value is true if the key is an extended key; otherwise, it is false.
bool prevdown :1; // Indicates whether the key is previously up or down
};

// Data of the char message
TCHAR ch;

// Data of the window message
struct
{
WPARAM wParam;
LPARAM lParam;
};
};
};

好吧,关于这个 union 的安全性问题。他也不整一个枚举来区分消息类型,反正你自己去用 WM_* 那些值判断吧,有些字段可能初始化了有些字段没初始化,哪些能用哪些不能用我也不告诉你,你 UB 关我屁事,自己动脑子想想,能跑就行。

还有那个虚拟键码和键盘扫描码。你猜猜刚入门的同学听得懂你在这里叽叽咕咕的那些东西不?同样是一个 u8 丢在那里就跑了。连个预处理都不肯做。简直懒的令人发指。

还是那句话:封装了约等于没有封装,简化了约等于没简化

别的什么诸如滥用全局状态,什么错误处理约等于没有之类的问题我真不想说了。基本的问题都解决不了,还指望这个作者能干什么呢。

可能这就是大佬吧,我的境界不够高,洞见不了他深邃的超凡脱俗的设计思想。

说实话我这段时间学 Vulkan API 的时候看那些东西都没有看 EasyX 那么难受。Vulkan 只是单纯的东西多而杂,不是烂,该有的设计规范人家一点不缺。

作者垃圾的运营能力

事情是不管的,惨是不哭不行的。代码是不放出来的,钱也是不赚的。问题是不修的,先把书卖出去就行。

我很好奇什么时候他能让大家 build from source。也不知道他抱着这个破烂的代码不放只提供静态库是准备待价而沽还是怎么的。谁去找他买这个玩意谁简直瞎了眼了。

其它

非常不利于学生进行图形学入门。也不利于 C++ 学习。

学完以后几乎获得不了任何有用的知识和设计经验。对于图形学原理的理解还是一坨浆糊。

结语:感觉不如

感觉不如 SFML。

个人认为 SFML 是一个非常优秀的入门级图形库。门槛很低,入门很简单。但上限也很高,可以接 ImGUI,可以接 Spine 做小人动画,可以写 GLSL 做花里胡哨的效果。学下来也会对图形学里面的一些基本概念有一个认知。API 设计也非常现代。

找一个最基本的功能来和 EasyX 比较一下。这是 SFML 的事件处理:

1
2
3
4
5
6
7
8
9
10
11
while (auto event = window->pollEvent()) {
if (event.has_value()) {
if (event->is<sf::Event::Closed>()) {
window->close();
return;
}
if (event->is<sf::Event::KeyPressed>()) {
sf::KeyBoard::Key keyCode = event->getIf<sf::Event::KeyPressed>()->code;
}
}
}

std::optional 一个可空,有事件就是非空,没有就是 std::nullopt 非常安全,而且显式。然后 Event::is() 内部是用模板元编程整的一个判断,如果判断成功了就返回变体类型 std::holds_alternative 的结果。 getIf 也是一个道理。非常现代,非常美观。KeyBoard::Key 是一个枚举类,里面哪个键是哪个映射的清清楚楚。

这不比 EasyX 强太多了。

我的建议是以后把 EasyX 踢到历史垃圾堆里面算了。反正我已经找到细糠吃了,EasyX 谁爱用谁用。这玩意我自己光看着都难受。


炮打 EasyX
https://lizi.moe/2025/06/30/炮打-EasyX/
作者
李萌
发布于
2025年6月30日
许可协议