创建一个具有专业外观(并且行为举止!)的表单设计师
当我开始编程时(大约 10 多年前),三件事让我感到惊讶:
- 编译器/解释器(当时我知道它们是“让我的程序运行的程序”,后面经常跟着限定词“无论它们是什么”)
- 代码编辑器
- 表单 。
那时,我将所有这些都视为生活的事实 我能够制作自己的专用程序,但是“使我的程序运行的程序”、代码编辑器和表单编辑器是上帝创造的,我无法干扰它们。
然后我上了大学,选修了形式语言处理课程。学习了形式语法、解析器、抽象语法树等之后;编译器、解释器和代码编辑器的所有魔力很快就消失了。编译器和解释器可以用理智且简单的方式编写,而语法突出显示代码编辑器可能需要的唯一不理智的事情就是 Windows API hack。
然而,直到今天,表单编辑器对我来说仍然是个谜。要么我缺乏制作表单设计器所需的技术知识,要么我有这些知识,但找不到使用它来实现表单设计器的方法。
使用 Visual C++ 和 MFC,我想实现一个受有史以来最好的表单设计器启发的表单设计器:
特别是,我想模仿它的两个我最喜欢的功能:
正在设计的表单位于容器内。因此,只需将容器大小调整到适当的大小,就可以设计任意大的表单,而不会浪费太多的屏幕空间。
“对齐网格”选项使设计专业外观的用户界面变得更加简单 令人沮丧。事实上,我什至可以说使用 Visual Basic 的窗体设计器创建具有专业外观的用户界面实际上是简单、有趣和令人愉快的。即使对于像我这样的左脑程序员来说也是如此。
所以,我有以下问题:
如何制作表单设计器,其中正在设计的表单位于容器内?正在设计的表单是否是包含在另一个窗口中的实际窗口?或者它只是表单设计器“手动”绘制的模型?
Windows API 和/或 MFC 是否包含函数、类以及任何可以轻松创建“可选择”项目的内容(在选择这些项目时会被白色或蓝色的小框包围,当它们被一个“抓住”时可调整大小)这些“边缘”)?
如何实现“对齐网格”功能?
When I began programming (some 10+ years ago), three things amazed me:
- Compilers/interpreters (back then I knew them as "programs that make my programs work", often followed by the qualifier "whatever they are")
- Code editors
- Form designers
Back then, I accepted all of them as facts of life. I was able to make my own special-purpose programs, but "programs that made my programs work", code editors and form editors were made by the Gods and there was no way I could mess with them.
Then I went to university, and took a course on formal language processing. After learning formal grammars, parsers, abstract syntax trees, etc.; all the magic about compilers, interpreters and code editors was soon gone. Compilers and interpreters could be written in sane and simple ways, and the only non-sane thing a syntax highlighting code editor could require were Windows API hacks.
However, to this day, form editors remain a mystery to me. Either I lack the technical knowledge required to make a form designer, or I have such knowledge, but cannot find a way to use it to implement a form designer.
Using Visual C++ and the MFC, I would like to implement a form designer inspired by the best form designer ever:
In particular, I would like to imitate its two features that I like the most:
The form being designed is inside a container. Thus, an arbitrarily large form may be designed without wasting too much screen real estate, by simply resizing the container to an appropriate size.
The "Align to Grid" option makes designing professional-looking user interfaces a lot less
frustrating. In fact, I would go as far as saying creating professional-looking user interfaces using Visual Basic's form designer is actually easy, fun and enjoyable. Even for left-brained programmers like me.
So, I have the following questions:
How do I make a form designer, in which the form being designed is inside a container? Is the form being designed an actual window contained inside another window? Or is it just a mockup "manually" painted by the form designer?
Do the Windows API and/or the MFC contain functions, classes, whatever that make it easy to create "selectable" items (surrounded by little white or blue boxes when they are selected, resizable when they are "grabbed" by one of these "edges")?
How do I implement the "Align to Grid" functionality?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
这里的两个答案都很好,但遗漏了我认为真正有趣的部分(包括一些您没有直接询问的部分,但无论如何您可能会感兴趣),所以这是我的 2c:
理想情况下绘制控件
,您只需继续创建该控件的常规实例即可。你想要看起来像按钮的东西吗?创建一个真正的按钮。棘手的事情是阻止它像按钮一样运行:您需要单击来激活它进行移动,而不是实际“单击”它。
处理这个问题的一种方法 - 假设所讨论的控件是“基于 HWND”(例如按钮、编辑、静态、列表框、树视图等的标准窗口集) - 是创建控件,然后子类化它 - 即。使用 SetWindowLongPtr(GWLP_WNDPROC, ...) 覆盖 wndproc,以便设计器代码可以拦截鼠标和键盘输入并使用它来启动移动,例如,而不是让鼠标输入传递到实际的按钮代码,这相反,会将其解释为“点击”事件。
子类化的另一种方法是在按钮上方放置一个不可见的窗口来捕获输入。拦截输入的想法相同,只是实现不同。
上述内容适用于托管(VB.Net、C#)和非托管(C/C++)控件;它们本质上都是库存窗口 HWND;托管版本只有一个托管包装器代码,该代码移交给底层的非托管控件。
旧的(预托管代码)ActiveX 控件,如 .Net VB 之前使用的那样,是完全不同的游戏。 ActiveX 容器和其中的 ActiveX 控件之间存在相当复杂的关系,其中许多 COM 接口处理属性协商、事件、绘制等事务。 (有一组接口允许 ActiveX 控件接收输入并自行绘制,而无需拥有自己的 HWND。)但是,您从这种复杂性中获得的一个好处是 ActiveX 控件具有显式的“设计模式”;它是一个简单的设计模式。因此控件知道在这种情况下做出适当的响应,并且可以配合整个过程。
表单本身...
所以基本上这些控件只是常规控件。那么您希望表单本身是常规表单吗? - 几乎。据我所知,它只是另一个基于 HWND 的窗口,它是设计器的子窗口(因此它会被剪切,并且可以在其中滚动);但我认为设计者在这里做了一些“作弊”,因为通常 Windows 只绘制像实际顶层窗口那样带有标题栏和最小/最大按钮的框架。我暂时不知道他们在这里使用的确切技术,但一些选项可能包括:手动绘制它以模仿 Windows 外观;使用Windows“主题”API,它允许您访问用于标题栏各个部分的图形元素,并将它们绘制在您想要的任何地方;或者,也许不太可能,将窗口设置为“MDI 子窗口” - 这是一种例外情况,窗口将在嵌套窗口周围绘制一个框架。
可拖动句柄
这里最简单的方法是设计者创建八个无标题栏的小方形弹出窗口,这些窗口位于所有其他元素之上 - 单击这些元素时会启动适当的调整大小代码。当用户从一个控件单击到另一个控件时,只需将拖动手柄窗口移动到当前活动的控件即可。 (请注意,在上述所有内容中,Windows 本身会计算出谁被单击,您无需实际将鼠标坐标与元素矩形坐标进行比较并自行计算出来。
)重新创建
对于非托管 C/C++ 使用的普通 Windows 系统控件,这相对容易:有一种众所周知的基于文本的文件格式 - .rc - 描述控件和位置。让设计者吐出这个(也可能是一个resource.h文件),你就完成了:任何C/C++项目都可以拾取这些文件并编译它们。托管代码(C#,VB.Net)有更多的内容复杂的方案,但它仍然是相同的基本思想:以托管工具期望的风格编写描述,他们会很乐意编译并使用它。
(ActiveX 控件 - 您已经猜到了 - 完全是另一个故事。据我所知,没有标准格式,因此使用数据的表单编辑器和运行时将紧密联系在一起 - 例如.Net VB6 之前的表单编辑器生成只有 VB 可以使用的表单 - 我认为已经有一段时间了...)
至于重新创建表单:如果您有一个 .rc 文件,它会被编译为对话框资源。 ,Windows 内置了对重新创建这些内容的支持。同样,托管代码支持库知道如何从特定格式重新创建表单。两者基本上都解析描述,并为每个项目创建适当类的元素,并根据指定设置适当的样式、文本和其他属性。它不会做任何你自己做不到的事情,它只是辅助实用程序代码。
处理焦点
对于任何容器中的 HWND 集合,无论是在“测试”模式还是在真实应用程序中实际运行,无论您是让 Windows 还是 Winforms 处理表单创建还是您自己创建每个 HWND,您都可以添加选项卡通过在消息循环中调用 IsDialogMessage 提供支持:请参阅 MSDN 页面备注部分了解详情。 (虽然 WinForms 可以做到这一点,但我认为它实际上有自己的焦点处理,因此它可以具有独立于视觉堆叠 Z 顺序的 Tab 键顺序。)
其他需要探索的事情...
与Spy++ 应用程序(SDK 的一部分,随 Visual Studio 一起安装)。如果您要使用 HWND 做任何事情,无论是托管还是非托管,那么了解如何使用此工具确实是个好主意:您可以将其指向 Windows 上的任何 UI 部分,并查看它是如何从一棵树中构建出来的。不同类型的 HWND。将其指向 VB 设计器,看看您自己到底发生了什么。 (单击工具栏上的“双筒望远镜”图标,然后将十字线拖到您感兴趣的窗口上。)
还要查看设计者吐出的资源文件。您可以在表单设计器中调整、移动或编辑的所有内容都对应于这些资源文件之一中的某个项目。制作它们的副本,调整一些设置,然后对两组进行文件比较,看看发生了什么变化。尝试手动更改文件中的一些内容(我认为它们几乎都是文本),重新加载,然后查看设计器是否接受了您的更改。
其他注意事项...
Much of the above is specific to Windows - notably the fact that since we're using Window's own building blocks - HWNDs - we can get Windows itself to do some of the hard work for us: it gives us the facilities to reuse the controls themselves at design time so we don't have to draw mock-ups; to intercept input on other controls so we can make a click into a move or whatever other action we want, or figure out which control is clicked without having to do the location math ourselves. If this was a designer for some other UI framework - say Flash - which doesn't use HWNDs internally, it would likely instead use that framework's own internal facilities to do similar work.
此外,如果将调色板中的控件数量限制为一个小的有限集,至少在开始时会容易得多。如果您想允许拖入任何控件 - 例如。第三方的,或者您在其他项目中使用过的;通常,您首先需要某种方式来“注册”该控件,以便设计者首先知道它是可用的。您可能还需要某种方法来发现它在工具栏中使用什么图标、它的名称是什么、它支持什么属性等等。
享受探索的乐趣!
Both the answers here are good, but left out what I consider to be the really interesting bits (including a couple that you didn't ask directly, but you might find of interest anyhow), so here's my 2c:
Drawing the controls
Ideally, you just go ahead and create a regular instance of the control. You want something that looks like a button? Create a real button. The tricky thing is stopping it from behaving like a button: you want clicks to activate it for moving, not actually 'click' it.
One way of dealing with this - assuming that the controls in question are 'HWND-based' (eg. the standard windows set of button, edit, static, listbox, treeview, etc.) - is to create the control, and then subclass it - ie. override the wndproc with SetWindowLongPtr(GWLP_WNDPROC, ...), so that the designer code can intercept mouse and keyboard input and use it to initiate a move, for example, instead of having the mouse input go through to the actual button code, which would instead interpret it as a 'click' event.
An alternative approach to subclassing is to place an invisible window above the button to capture the input. Same idea of intercepting input, just different implementation.
The above applies to both managed (VB.Net, C#) and unmanaged (C/C++) controls; they're both essentially stock windows HWNDs; the managed versions just have a managed wrapper code handing off to the underlying unmanaged control.
The old (pre-managed code) ActiveX controls, as used in pre-.Net VB, were a whole different ball game. There's a fairly complex relationship between an ActiveX container and the ActiveX controls within it, with many COM interfaces handling things like negotiation of properties, events, painting, and so on. (There's event a set of interfaces that allows an ActiveX control to receive input and draw itself without having its own HWND.) One benefit you get from this complexity, however, is that ActiveX controls have an explicit 'design mode'; so a control knows to respond appropriately in that case and can cooperate with the whole procedure.
The Form Itself...
So basically the controls are just regular controls. So you'd expect the form itself to be a regular form? - Almost. As far as I know, its just another HWND-based window, that's a child of the designer (so it gets clipped, and can be scrolled within it); but I think the designer is doing a bit of 'cheating' here, because usually Windows only draws frames like - with titlebar and min/max buttons that for actual top-level windows. I don't know offhand the exact technique that they're using here, but some options could include: painting it manually to mimic the Windows look; using the Windows "theme" APIs, which allow you to access the graphic elements used for the bits and pieces of titlebars and paint them wherever you want to; or, perhaps less likely, setting the window up as a "MDI Child window" - this is one exception case where windows will draw a frame around a nested window.
Draggable Handles
Simplest approach here is for the designer to create eight small square title-bar-less popup windows that sit above all the other elements - which initiate the appropriate resize code when they are clicked. As the user clicks from control to control, just move the drag handle windows to the currently active control. (Note that in all the above, Windows itself is figuring out who's been clicked, you never have to actually compare mouse coords against element rectangle coordinates and work it out yourself.)
Saving & Recreating
For plain windows system controls that are used by unmanaged C/C++, it's relatively easy: there's a well-known text-based file format - .rc - that describes the controls and locations. Have the designer spit out that (and likely a resource.h file also) and you're done: any C/C++ project can pick up those files and compile them in. Managed code (C#, VB.Net) has a somewhat more complex scheme, but it's still the same basic idea: write out a description in the style that the managed tools expect, and they'll happily compile it and use it.
(ActiveX controls are - you've guessed it - a whole 'nother story. There isn't a standard format that I'm aware of, so the form editor and runtime that consumes the data would be closely tied together - eg. the form editor from pre-.Net VB6 produces forms that only VB can use. - I think. It's been some time ago...)
As for recreating the form: if you have a .rc file, it gets compiled into a dialog resource, Windows has built in support to recreate those. Likewise, the managed code support libraries know how to recreate a form from its particular format. Both basically parse the description, and for each item, create elements of the appropriate classes, and set the appropriate style, text, and other properties as specified. It's not doing anything you can't do yourself, its just helper utility code.
Handling Focus
For a collection of HWNDs in any container, whether in 'test' mode or actually running in the real app, and regardless of whether you let Windows or Winforms handle the form creation or whether you created each HWNDs yourself, you can add tabbing support by calling IsDialogMessage in your message loop: see the MSDN page remarks section for details. (While WinForms could do this, I think it actually does its own focus handling, so that it can have tab order independent from visual stacking Z-Order.)
Other Things To Explore...
Make friends with the Spy++ app (part of the SDK, installs with Visual Studio). If you're going to do anything with HWNDs, managed or unmanaged, it's a real good idea to know how to use this tool: you can point it at any piece of UI on Windows, and see how it's constructed out of a tree of different types of HWNDs. Point it at the VB designer and see what's really happening for yourself. (Click the 'binoculars' icon on the toolbar, then drag the crosshairs the the window you're interested in.)
Also take a look at the resource files that the designer spits out. Everything that you can tweak or move or edit in the forms designer corresponds to some item somewhere in one of those resource files. Make a copy of them, tweak some settings, and then file-compare the two sets, and see what's changed. Try changing some things in the files by hand (I think they're nearly all text), reload, and see if the designer picked up your changes.
Other Things To Note...
Much of the above is specific to Windows - notably the fact that since we're using Window's own building blocks - HWNDs - we can get Windows itself to do some of the hard work for us: it gives us the facilities to reuse the controls themselves at design time so we don't have to draw mock-ups; to intercept input on other controls so we can make a click into a move or whatever other action we want, or figure out which control is clicked without having to do the location math ourselves. If this was a designer for some other UI framework - say Flash - which doesn't use HWNDs internally, it would likely instead use that framework's own internal facilities to do similar work.
Also, it's far easier if you limit the number of controls in the palette to a small finite set, at least at first. If you want to allow any control at all to be dragged in - eg. a 3rd party one, or one you've used in another project; you typically first need some way for that control to be 'registered' so that the designer knows that it's available in the first place. And you may also need some way to discover what icon it uses in the toolbar, what its name is, what properties it supports - and so on.
Have fun exploring!
您可以像实现普通 GUI 一样实现表单设计器。你有可以拖动的东西(你的小部件),你有你可以点击的东西(你的按钮),你有你可以选择的东西(你放置的小部件),这就是真正的事情。
问:现在,如何在 GUI 中显示窗口?
A:你画它,就这么简单。
问:如何将东西保存在窗口内?
答:您检查“父”对象的边界。您几乎可以说,表单设计器就像一个小游戏,您有一个包含所有小部件的场景图,通过父子关系连接。
问:那么,如何在 GUI 中选择内容?
答:单击时检查当前鼠标位置与所有(附近)小部件的边界(场景图仅在此处有帮助,如四叉树)。
问:如何在网格上对齐小部件?
A:对于网格对齐,我们举一个简单的例子:假设您的 X 轴实际分辨率为
100px
,但您希望网格的分辨率仅为10px
> 关于 x。现在假设您以实际分辨率将小部件移动28px
。要获得网格分辨率,您只需除以10
,得到2.8
,对其进行舍入,最后将小部件3
移动到 x 上。四舍五入是这里的关键。仅当网格移动>= ?.5
时,您才会捕捉到下一个图块。否则你就简单地呆在旧的地方。希望这能为您提供有关如何开始表单设计器的一般提示。玩得开心。 :)
(PS:抱歉,不知道任何特定的 WinAPI/MFC 函数/类可以帮助您。)
You implement a form designer almost like a normal GUI. You have stuff you can drag (your widgets), you have stuff you can click (your buttons) and you have stuff you can select (your placed widgets) and that's really about it.
Q: Now, how do you display a window in a GUI?
A: You paint it, simple as that.
Q: And how do you keep stuff inside that window?
A: You check against the bounds of the "parent" object. You could almost say that a form designer is like a little game and you have a scene graph holding all your widgets, connected by parent-child-relations.
Q: Then, how do you select stuff in a GUI?
A: Check the current mouse position on-click against the bounds of all (near) widgets (a scene graph only helps here, like a quadtree).
Q: How do you align widgets on a grid?
A: For the grid alignment, let's have a simple example: Say your real resolution is
100px
on the x-axis, but you want your grid to only have a resolution of10px
on x. Now say you move your widget by28px
in real resolution. To get the grid resolution, you simply divide by10
, get2.8
, round that, and finally move the widget3
tiles on x. The rounding is the key here. only if the grid movement is>= ?.5
, you snap to the next tile. Otherwise you simple stay at the old one.Hope this can give you a general hint on how to start a form designer. Have fun. :)
(PS: Don't know about any specific WinAPI/MFC functions/class to help you along, sorry.)
只是对@Xeo 已经说过的内容添加一两点:
首先,不,你并不总是自己绘制所有内容。在正常的设计阶段,您基本上只是绘制一些看起来像控件的东西,但是(至少 IIRC)它还允许您在测试模式下“运行”表单(当然 VC++ 对话框设计器会这样做,尽管 VB 更原始) ,我认为它也确实具有这种特殊功能)。测试模式是指您可以在(必须)附加任何代码之前“运行”表单 - 即使单击按钮(例如)不会在周围的程序中执行任何操作,控件本身也可以正常工作-- 按钮正常单击,编辑控件将允许您进行编辑等。
这是通过实际实例化控件,告诉它正确的位置、大小和属性来完成的。 ActiveX 控件做了相当多的工作来支持这一点,以前的“Windows 自定义控件”也做了同样的事情,尽管复杂程度要低得多。从控件的角度来看,它的工作方式与平常完全一样,接收输入,向其父级发送通知等。唯一改变的是父级忽略了它收到的大部分通知,但控件不这样做真的不知道。
有两种基本方法可以做到这一点。一种是自己创建一个控件集合,以及它们的位置、大小等,然后使用 CreateWindow(或 CreateWindowEx 等)来创建正确的窗口班级。虽然相对容易处理,但它的缺点是将所有选项卡处理留给您。
另一种可能性是创建一个
DLGTEMPLATE
结构来保存有关对话框的数据,并为各个控件创建一些DLGITEMTEMPLATES
,最后使用CreateDialogIndirect
> 创建一个具有这些规格的对话框,并保留这些控件。这很乏味,但是很有效,并且当您完成后,它会自动处理控件之间的选项卡(并且与任何其他对话框的工作方式相同,因为无论哪种方式都是使用相同的 Windows 代码创建它)。其次,既然您已经标记了这个 C++,您可能想看一下一些代码< /a> 在 CodeProject 上,它实际上实现了一个对话框编辑器。尽管它不像某些商业编辑器那么复杂,但这是一个相当完整的表单/对话框编辑器,包含您所询问的大部分内容。
Just to add a point or two to what @Xeo has already said:
First of all, no, you don't always draw all the content yourself. During normal design phase you're basically just drawing something that looks like the control, but (at least IIRC) it also lets you "run" a form in test mode (certainly the VC++ dialog designer does, and even though VB's was more primitive, I think it did have that particular capability as well). Test mode was when you can "run" a form before you've (necessarily) attached any code to it -- even though clicking a button (for example) doesn't do anything in the surrounding program, the control itself works as normal -- a button clicks normally, an edit control will let you edit, etc.
That is done by actually instantiating a control, telling it the correct position, size, and properties. ActiveX controls do quite a bit to support this, and the previous "Windows custom controls" did as well, though with considerably less sophistication. From the viewpoint of the control, it's working just about exactly like it normally would, receiving input, sending notification to its parent, etc. The only thing that's changed is that the parent most ignores most of the notifications it receives, but the control doesn't really know that.
There are two basic ways to do this. One is to create a collection of controls yourself, along with their positions, sizes, etc., and use
CreateWindow
(orCreateWindowEx
, etc.) to create window of the correct class. While relatively easy to handle, this has the disadvantage that it leaves all the tab-handling to you.The other possibility is to create a
DLGTEMPLATE
structure to hold data about the dialog box, and a someDLGITEMTEMPLATES
for the individual controls, and finally useCreateDialogIndirect
to create a dialog box with those specs, holding those controls. It's tedious, but it works, and when you're done it handles tabbing between controls automatically (and works the same as any other dialog, since it's the same Windows code creating it either way).Second, since you've tagged this C++, you might want to take a look at some code on CodeProject that actually implements a dialog editor. Although it isn't quite as sophisticated as some of the commercial ones, this is a reasonably complete form/dialog editor, complete with most of what you've asked about.