使用矢量艺术创建备用 GUI





5.00/5 (20投票s)
2000年1月30日

247679

7320
创建视觉复杂但程序简单的非矩形 GUI
引言
作为 Windows 程序员,我们知道以下事实是真的
- 所有窗口都必须是矩形的。
- 所有 GUI 组件都必须具有 3D 雕刻铬外观。
- 如果 GUI 不遵循最新 MS-Office 套件或最新 IE 版本的样式,就会显得过时和陈旧。
- 如果您偏离上述规定,将需要大量工作,增加大量错误,但功能上的好处却很少或没有。
幸运的是,这些“事实”并非如此。我个人认为,我们正开始从硬编码、类似微软的 GUI 过渡到更独特、有时甚至是照片级逼真 GUI。目前,您可以在“皮肤化”的应用程序和像照片级逼真 Gizmos 这样的商业应用程序中看到这种趋势。当 Apple 的 OS X 发布一年左右时,我相信我们将在 Apple 和 Microsoft 平台看到备用 GUI 的爆炸式增长。
可用于备用 GUI 的技术
我认为我作为一名软件工程师的工作之一就是为未来做准备。为了准备,我一直在研究一些技术
- GDI 图形 - 这意味着使用
MoveTo
、LineTo
和其他熟悉的 GDI 调用对组件进行硬编码。CodeProject.com 上有关自定义 GUI 组件的大部分文章都使用了这种技术。这种技术非常灵活,但更改会直接影响编译后的代码。 - 位图图形 - 该技术使用位图和命中区域来创建 GUI 组件。这是一种常见技术——CodeProject.com 上可以找到几篇讨论此技术的文章。此技术可与“皮肤化”结合使用,在运行时应用 GUI 的实际图形元素。
WinAmp
是使用此技术的一个很好的例子。这种技术很简单(尽管我不会说它微不足道)。但是,位图的可伸缩性不好,因此如果您的 GUI 需要调整大小,您通常需要为每个所需的大小定义位图。 - 矢量图形 - 矢量图形组件对于图形艺术家来说创建起来和位图图形一样容易。借助正确的工具,可以轻松地将矢量图形转换为可直接在您的应用程序中使用的格式。矢量图形具有几个优点。它们易于绘制,易于转换为区域,易于进行命中测试,并且易于平移、旋转和缩放。本文展示了如何使用此技术来创建有趣的 GUI。这种方法应该可以与“皮肤化”结合使用。希望您能看到矢量图形在应用程序中是多么容易使用。
- 渲染图形 - 该技术允许 GUI 组件即时渲染。这意味着图像组件的定义是使用 DirectX 或 OpenGl 等渲染引擎进行渲染的。该技术目前应用于游戏。但是,没有理由认为照片级逼真的 GUI 组件不能使用相同的方法。这种方法应该可以与“皮肤化”结合使用。
使用矢量图创建 GUI
如何获取矢量图
我所在的公司有一个幸运的机会,可以与工业艺术家和人类因素工程师密切合作。在过去的几年里,我工作的一部分是将工业艺术家的艺术和人类因素工程师的行为转换为 Windows 应用程序的元素。这加强了我的观点,即大多数程序员都是糟糕的艺术家,甚至更糟糕的界面设计者(所以您知道,我也包括在我这个广泛、过于简化的概括中)。
在我公司,工业艺术家首选的工具是 Adobe Illustrator。该工具允许创建位图、矢量图或两者的组合。我的观察是,艺术家更喜欢使用矢量图。这是出于实际原因。矢量图可以定义为可以独立排序、平移、旋转和缩放的单个对象。
我还观察到 Windows 程序员存在心理障碍。所有图形问题都用位图解决。个人而言,我讨厌位图。它们不易缩放或旋转。它们往往很大。而且,如果您想为它们定义任何有趣的交互行为,您必须将命中区域与位图关联起来。换句话说,位图很难处理,结果往往很差。
由于我认识的艺术家们无论如何都更喜欢使用矢量图,因此我一直在为我的项目索要他们艺术作品的矢量形式。然后,我通过一个我编写的工具运行这些文件,该工具提取矢量图和文本,并将它们转换为 C++ 代码。本文展示了如何使用此类数据来创建有趣的应用程序。
免责声明:请勿假设这里展示的项目是好的艺术或好的人类因素设计的示例。它只是一个如何使用矢量图的示例。
以矢量图思维
矢量图的绘制方式与画家绘画相同。图像的元素是从后往前绘制的,后面的元素可能会遮挡前面的某些元素。这意味着您永远不应更改绘制项目的顺序。这也意味着您应该按照与绘制顺序相反的顺序进行命中测试。
一个多边形转换示例
以下是一个红色圆圈的输出示例。我用 CorelDraw 绘制了该图形。将其输出保存为 Windows MetaFile。然后,我通过我编写的一个工具运行该文件,该工具提取多边形、折线和文本。以下代码是该工具的输出。
#include "StdAfx.h"
#include "ArtCode.h" // Contains code that does hit-test, drawing, and region
creation.
static RECT bounds = {20, 20, 15980, 15740};
static POINT Polygon0Points[] = {
{8000, 15740}, {8409, 15730}, {8814, 15700}, {9212, 15650}, {9605, 15580},
{9990, 15492}, {10368, 15386}, {10739, 15262}, {11101, 15121}, {11454, 14963},
{11798, 14789}, {12133, 14600}, {12456, 14395}, {12769, 14176}, {13071,
13942}, {13361, 13694}, {13638, 13434}, {13903, 13161}, {14154, 12875},
{14391, 12578},
{14614, 12270}, {14822, 11951}, {15015, 11622}, {15191, 11283}, {15351,
10935}, {15495, 10578}, {15620, 10213}, {15728, 9841}, {15818, 9461},
{15888, 9074},
{15939, 8682}, {15970, 8284}, {15980, 7880}, {15970, 7477}, {15939, 7078},
{15888, 6686}, {15818, 6299}, {15728, 5920}, {15620, 5547}, {15495, 5182},
{15351, 4825}, {15191, 4478}, {15015, 4139}, {14822, 3810}, {14614, 3490},
{14391, 3182}, {14154, 2885}, {13903, 2600}, {13638, 2326}, {13361, 2066},
{13071, 1819}, {12769, 1585}, {12456, 1365}, {12133, 1160}, {11798, 971},
{11454, 797}, {11101, 639}, {10739, 498}, {10368, 374}, {9990, 268},
{9605, 180}, {9212, 111}, {8814, 60}, {8409, 30}, {8000, 20}, {7590,
30}, {7186, 60}, {6787, 111}, {6395, 180}, {6009, 268},
{5631, 374}, {5261, 498}, {4899, 639}, {4545, 797}, {4201, 971}, {3867,
1160}, {3543, 1365}, {3230, 1585}, {2929, 1819}, {2639, 2066},
{2361, 2326}, {2097, 2600}, {1846, 2885}, {1608, 3182}, {1386, 3490},
{1178, 3810}, {985, 4139}, {808, 4478}, {648, 4825}, {505, 5182},
{379, 5547}, {272, 5920}, {182, 6299}, {112, 6686}, { 61, 7078}, { 30,
7477}, { 20, 7880}, { 30, 8284}, { 61, 8682}, {112, 9074},
{182, 9461}, {272, 9841}, {379, 10213}, {505, 10578}, {648, 10935}, {808,
11283}, {985, 11622}, {1178, 11951}, {1386, 12270}, {1608, 12578},
{1846, 12875}, {2097, 13161}, {2361, 13434}, {2639, 13694}, {2929, 13942},
{3230, 14176}, {3543, 14395}, {3867, 14600}, {4201, 14789}, {4545, 14963},
{4899, 15121}, {5261, 15262}, {5631, 15386}, {6009, 15492}, {6395, 15580},
{6787, 15650}, {7186, 15700}, {7590, 15730}, {8000, 15740},
};
static PolygonEntry Polygon0 = {
"Polygon0",
{20, 20, 15980, 15740},
RGB(218, 37, 29),
1, // PolyFillMode
129,
Polygon0Points,
};
// Draw Vector image - will scale to clientRect so if you want
// to maintain the images aspect ratio please make sure the clientRect
// passed also maintains that aspect ratio of the object.
void DrawObject(CDC* pDC, const CRect& clientRect) {
Draw(pDC, Polygon0, clientRect, bounds);
}
// Creates a region. Regions can be used for several things.
// However the result of this call is usually used to create a
// Window Region.
void CreateRegion(CRgn& rgn, const CRect& clientRect) {
CreateRegion(rgn, Polygon0, clientRect, bounds);
}
// TRUE on an element in the vector drawing is hit.
BOOL HitTestObject(const CPoint& point, const CRect& clientRect) {
if (HitTest(Polygon1, clientRect, bounds, point)) return TRUE;
return FALSE;
}
// Returns the aspect ratio of the image. To use this you do the following:
// newWidth = AspectRatio()*window.Height();
double AspectRatio() {
CRect objectRect(bounds);
return ((double) objectRect.Width())/((double) objectRect.Height());
}
创建对话框应用程序
要修改您的对话框应用程序,您需要做的就是将窗口区域设置为组合矢量图的区域,然后处理 OnPaint
消息。设置窗口区域的逻辑位置是在 OnInitDialog
中。
OnInitDialog
此代码执行以下操作:
- 将窗口矩形设置为矢量图的纵横比
- 获取绘图中组合矢量图的区域
- 将窗口区域设置为绘图区域
BOOL CFlowerPowerDlg::OnInitDialog()
{
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
m_downItem = None;
// Maintain aspect ratio of drawing
CRect rect;
GetWindowRect(&rect);
rect.right = ((int) (rect.Height()*AspectRatio()))+rect.left;
CRgn wndRgn;
// Size Window to new aspect ratio
MoveWindow(&rect, FALSE);
// Get Screen Rect and convert to client coords
GetWindowRect(&rect);
ScreenToClient(&rect);
// Create a region for our window in client coords
CreateRegion(wndRgn, rect);
// Set Window region in client coords - the MS docs say this call is in
// Screen Coords, but only client coords work.
SetWindowRgn(wndRgn, TRUE);
CDialog::OnInitDialog();
return TRUE; // return TRUE unless you set the focus to a control
}
OnPaint
OnPaint
是 AppWizard
为非图标绘制生成的标准输出。绘制对话框的非图标版本时,您执行以下操作:
- 获取 Windows 的 DC
- 创建内存 DC(以消除重绘时的闪烁)
- 获取窗口矩形(我们正在接管整个对话框区域,而不仅仅是客户区域)
- 转换为客户坐标
- 绘制矢量图像
void CFlowerPowerDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
if (!m_bDialogUp) {
CWindowDC dc(this);
CMemDC pDC(&dc);
CRect rect;
GetWindowRect(&rect);
ScreenToClient(&rect);
DrawObject(&pDC, rect);
CDialog::OnPaint();
}
}
}
创建您自己的矢量代码
要将您自己的 C++ 代码从矢量图创建出来,我包含了名为 ArtCode.exe 的应用程序。请注意,此应用程序是一个完整的“hack”。它的设计目的是为了用我已有的工具尽可能少地工作,将矢量图转换为代码。我拥有 Adobe Illustrator 和 CorelDraw。因此,将矢量图转换为 C++ 的过程需要一个相对较新的 CorelDraw 版本(我成功使用了 6-9 版本)。以下是过程(是的,它很古怪——但它有效)
- 将您的矢量艺术导入 CorelDraw。
- 选择要转换为代码的组件。
- 将选定的图形组件导出为 Adobe Illustrator 格式(此步骤将图形原语的数量减少到 ArtCode.exe 程序支持的那些)。
- 导入该 Adobe Illustrator 文件。
- 将艺术作品导出为 Windows Metafile(我建议将文本转换为曲线)。
- 使用 ArtCode.exe 打开 Windows Metafile。
- 选择“编辑”|“复制”命令(这将生成代码并将其放入剪贴板)。
- 将代码粘贴到 VC++ 中。
- 将
FlowerPower
项目中的 ArtCode.h 和 ArtCode.cpp 文件添加到您的项目中。
额外资源
矢量图的旋转、平移和缩放很容易,但如果您不记得线性代数课程(我的是 20 多年前的)中的方法,您将需要一本好的计算机图形学书籍或一本好的线性代数书籍。您只需设置一个变换矩阵并进行矩阵乘法即可执行一个或多个这些操作。一旦您看到它,您就会发现它有多么简单。一个不错的计算器也有帮助(我喜欢我的 TI-92)。
关于工具的评论
ArtCode.exe 程序完全不受支持。请不要给我发送电子邮件要求更改或帮助您解决遇到的任何问题。我极不可能回复任何此类电子邮件。如果您确实遇到困难并且绝对需要帮助,您可以尝试 CodeProject.com 上的讨论论坛。
如果您尝试打开普通的 Windows Metafile,该工具将无法工作。该工具仅支持非常非常少量的 Windows Metafile 命令,因此如果您导入典型的 WMF,您很有可能得不到任何图形或只能得到部分图形。在大多数情况下,首先导出为 Adobe Illustrator 格式的步骤是绝对必需的。
结论
本文的重点不是引起 GUI 设计方式的任何重大改变。目标是激发对 GUI 开发未来走向的思考,并帮助您为即将到来的未来做好准备。它还应该向您展示使用矢量图形比使用位图图形容易得多。
注意事项
您可以随意使用项目中包含的代码。但是,这些艺术作品来自我合法的 CorelDraw 副本。我相信使用衍生版本的艺术作品在我的代码中是合法的,但是,除非您拥有 CorelDraw,否则请不要在您的项目中使用这些艺术作品。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。