第四章 2D 图形测试平台
使用 2D测试平台
在书的官网 cgpp.net 提供了 2D 图形测试平台,我们只需要下载其源码,然后根据需要,将里面的东西放入到自己的 WPF 项目中即可。
割角
打开在官网下载的文件,找到 Subdiv 文件夹,打开里面的 vs项目,并生成 exe 文件。可以先运行一下,体验一下:
打开项目,查看 Windowl.xaml 代码,找到 Subdivide 和 Clear 按钮的代码:
这两个按钮分别绑定了 b1Click、b2Click 这两个事件,当点击按钮时,事件会传递给程序
再来查看 Windowl.xaml.cs 代码。
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Diagnostics;
namespace GraphicsBook
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Polygon myPolygon = new Polygon();
Polygon mySubdivPolygon = new Polygon();
bool isSubdivided = false;
GraphPaper gp = null;
// Are we ready for interactions like slider-changes to alter the
// parts of our display (like polygons or images or arrows)? Probably not until those things
// have been constructed!
bool ready = false;
// Code to create and display objects goes here.
/// <summary>
/// Create a window containing a polygon (with no vertices) to which the user can ad verts with
/// left-clicks. A click on the "subdivide" button makes a subdivided polygon appear in dark red,
/// with the original in black. Subsequent clicks make the red polygon black, and create a new
/// sub-sub-divided polygon, and so on.
/// </summary>
public MainWindow()
{
InitializeComponent();
InitializeCommnds();
InitializeInteraction();
// Now add some graphical items in the main Canvas, whose name is "GraphPaper"
gp = this.FindName("Paper") as GraphPaper;
initPoly(myPolygon, Brushes.Black);
initPoly(mySubdivPolygon, Brushes.Firebrick);
gp.Children.Add(myPolygon);
gp.Children.Add(mySubdivPolygon);
ready = true; // Now we're ready to have sliders and buttons influence
the display.
}
/// <summary>
/// Initialize the polygon p to have a standard stroke-thickness and mitered corners,
/// and give it the Stroke style specified by the brush b.
/// </summary>
/// <param name="p">A polygon whose properties we set</param>
/// <param name="b">The stroke style to use</param>
private void initPoly(Polygon p, SolidColorBrush b)
{
p.Stroke = b;
p.StrokeThickness = 0.5; // 0.25 mm thick line
p.StrokeMiterLimit = 1; // no long pointy bits at vertices
p.Fill = null;
}
#region Interaction handling
/// <summary>
/// Handle clicks on the "subdivide" button. If the current polygon has been subdivided,
/// make the subdivided polygon the current one, and create a more finely subdivided one to be the "subdivided"
/// polygon.
///
/// If the current polygon has not been subdivided, then subdivide it (assuming it has more than zero
/// points), and set "isSubdivided" to true, so that further left-clicks are disabled.
///
/// Assign colors to the current and subdivided polygons as well.
/// </summary>
/// <param name="sender">The "Subdivide" button</param>
/// <param name="e">The click-event</param>
public void b1Click(object sender, RoutedEventArgs e)
{
Debug.Print("Subdivide button clicked!\n");
if (isSubdivided)
{
myPolygon.Points = mySubdivPolygon.Points;
mySubdivPolygon.Points = new PointCollection();
}
int n = myPolygon.Points.Count;
if (n > 0)
{
isSubdivided = true;
}
for (int i = 0; i < n; i++)
{
int lasti = (i + (n - 1)) % n ; // index of previous point
int nexti = (i + 1) % n; // index of next point.
double x = (1.0f / 3.0f) * myPolygon.Points[lasti].X + (2.0f / 3.0f)
* myPolygon.Points[i].X;
double y = (1.0f / 3.0f) * myPolygon.Points[lasti].Y + (2.0f / 3.0f)
* myPolygon.Points[i].Y;
mySubdivPolygon.Points.Add(new Point(x, y));
x = (1.0f / 3.0f) * myPolygon.Points[nexti].X + (2.0f / 3.0f) *
myPolygon.Points[i].X;
y = (1.0f / 3.0f) * myPolygon.Points[nexti].Y + (2.0f / 3.0f) *
myPolygon.Points[i].Y;
mySubdivPolygon.Points.Add(new Point(x, y));
}
e.Handled = true; // don't propagate the click any further
}
// Clear button
/// <summary>
/// Handle clicks on the "Clear" button: set isSubdivided to false, and remove both the current
/// polygon and the subdivided one (in the sense of removing all their vertices).
/// </summary>
/// <param name="sender">The "Clear" button</param>
/// <param name="e">The click-event</param>
public void b2Click(object sender, RoutedEventArgs e)
{
Debug.Print("Clear button clicked!\n");
myPolygon.Points.Clear();
mySubdivPolygon.Points.Clear();
isSubdivided = false;
e.Handled = true; // don't propagate the click any further
}
#endregion
#region Menu, command, and keypress handling
protected static RoutedCommand ExitCommand;
protected void InitializeCommands()
{
InputGestureCollection inp = new InputGestureCollection();
inp.Add(new KeyGesture(Key.X, ModifierKeys.Control));
ExitCommand = new RoutedCommand("Exit", typeof(MainWindow), inp);
CommandBindings.Add(new CommandBinding(ExitCommand, CloseApp));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Close,
CloseApp));
CommandBindings.Add(new CommandBinding(ApplicationCommands.New,
NewCommandHandler));
}
protected void InitializeInteraction()
{
MouseLeftButtonDown += MouseButtonDownA;
MouseLeftButtonUp += MouseButtonUpA;
MouseMove += RESPOND_MouseMoveA;
}
void NewCommandHandler(Object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("You selected the New command",
Title,
MessageBoxButton.OK,
MessageBoxImage.Exclamation);
}
// Announce keypresses, EXCEPT for CTRL, ALT, SHIFT, CAPS-LOCK, and "SYSTEM" (which is how Windows
// seems to refer to the "ALT" keys on my keyboard) modifier keys
// Note that keypresses that represent commands (like ctrl-N for "new") get trapped and never get
// to this handler.
void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.Key != Key.LeftCtrl) &&
(e.Key != Key.RightCtrl) &&
(e.Key != Key.LeftAlt) &&
(e.Key != Key.RightAlt) &&
(e.Key != Key.System) &&
(e.Key != Key.Capital) &&
(e.Key != Key.LeftShift) &&
(e.Key != Key.RightShift))
{
MessageBox.Show(String.Format("[{0}] {1} received @ {2}",
e.Key,
e.RoutedEvent.Name,
DateTime.Now.ToLongTimeString()),
Title,
MessageBoxButton.OK,
MessageBoxImage.Exclamation);
}
}
void CloseApp(Object sender, ExecutedRoutedEventArgs args)
{
if (MessageBoxResult.Yes ==
MessageBox.Show("Really Exit?",
Title,
MessageBoxButton.YesNo,
MessageBoxImage.Question)
) Close();
}
#endregion //Menu, command and keypress handling
#region Mouse Event Handling
public void MouseButtonUpA(object sender, RoutedEventArgs e)
{
if (sender != this) return;
System.Windows.Input.MouseButtonEventArgs ee =
(System.Windows.Input.MouseButtonEventArgs)e;
Debug.Print("MouseUp at " + ee.GetPosition(this));
e.Handled = true;
}
/// <summary>
/// Handle a left mouse-click by adding a new vertex to the current polygon, as long as
/// no subdivision has yet occured
/// </summary>
/// <param name="sender">The GraphPaper object</param>
/// <param name="e">The mouse-click event</param>
public void MouseButtonDownA(object sender, RoutedEventArgs e)
{
Debug.Print("Mouse down");
if (ready)
{
if (sender != this) return;
System.Windows.Input.MouseButtonEventArgs ee =
(System.Windows.Input.MouseButtonEventArgs)e;
Debug.Print("MouseDown at " + ee.GetPosition(this));
if (!isSubdivided)
{
myPolygon.Points.Add(ee.GetPosition(gp));
}
}
e.Handled = true;
}
public void RESPOND_MouseMoveA(object sender, MouseEventArgs e)
{
if (sender != this) return;
System.Windows.Input.MouseEventArgs ee =
(System.Windows.Input.MouseEventArgs)e;
// Uncommment following line to get a flood of mouse-moved messages.
// Debug.Print("MouseMove at " + ee.GetPosition(this));
e.Handled = true;
}
#endregion
}
}
暂时了解即可。这里举例这只是为了让读者熟悉 2D图形测试平台。
坐标系
对于传统的笛卡尔坐标系,y 轴垂直向上。
而 WPF 的坐标系,y 轴则是垂直向下。
因为 y 轴分量增加方向朝下更能自然地表达另一些东西,例如 矩阵的行和列
WPF数据依赖
三角形是一个含有三个点(Point) 的 Polygon。WPF 内置了一种能力,当子元素的状态发生改变时,会重新绘制。当我们将 三角形 加入 GraphPaper 的子元素集时,画布就会绘制出三角形,而如果我们将 三角形 从子元素集中删除,画布会重新绘制,三角形消失。
而 ”查看子元素的状态有无改变“只能查看某一固定的层次。如果组成 三角形(Polygon) 的 Point 集有所改变,这属于 Polygon 自身的改变,GraphPaper 并不会重绘。
只有”组成集合的子元素的索引有所改变“才被视为 集合(Collection) 的改变。
因此,我们先把要改变的 Point 删除,再插入一个新的 Point,该 Point 的位置就是我们要改变的位置,这样就会进行重绘。
事件处理
WPF 接受用户以键盘按键、鼠标点击、鼠标拖动等形式对系统进行交互,称为事件。
当检测到一个事件时,WPF会调用相应的事件处理器。
有些事件处理器是一个组件,有些是回调函数
public void b1Click(object sender, RoutedEventArgs e)
{
Debug.Print("Button one clicked\n");
e.Handled = true;
}
代码中的 sender 是 WPF 中的 实体,点击事件由它传递过来。
当点击位于画布某一网格点的按钮上的文字时,会依次触发 文字对象、按钮、格点 和 画布 的反应。 如果要终止传递,就要把 RoutedEventArgs 对象的 Handled 变量设置为 true
动画
用户可以在 C# 或 XAML 中定义动画。
在 XAML 中,有许多预定义动画,可以对他们进行组合生成更复杂的动画。
在 C# 中,既可以使用预定义动画,也可以编写任意复杂的程序创建自己的动画。
PointAnimation animaPoint1 = new PointAnimation(
new Point(-20, -20),
new Point(-40, 20),
new Duration(new TimeSpan(0, 0, 5)));
animaPoint1.AutoReverse = true;
animaPoint1.RepeatBehavior = RepeatBehavior.Forever;
p1.BeginAnimation(Dot.PositionProperty, animaPoint1);
上述 C# 代码创建了一个 点实例,并且使其具有往复的动画
交互
在主 Window 中任何地方按压按键都被分为两个阶段处理:
首先,其中一部分会被识别为 命令 (例如,“Alt + X” 表示 “退出程序”);
其次,未被识别为命令的按键动作由 KeyDownHandler 处理,该方法会对所有的按键做出响应,或者予以忽略处理(对于 Control 或者 Shift 之类的修饰键)
回到割角程序
首先,创建基础画布和按钮:
<Window x:Class="GraphicsBook.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:k="clr-namespace:GraphicsBook;assembly=Testbed2D"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:GraphicsBook"
mc:Ignorable="d"
Title="Window1" Height="450" Width="800">
<DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Left"
Orientation="Vertical" Background="#ECE9D8">
<TextBlock Margin="3" Text="Controls"/>
<Button Margin="3, 5" HorizontalAlignment="Left"
Click="b1Click">SubDivde</Button>
<Button Margin="3, 5" HorizontalAlignment="Left"
Click="b2Click">Clear</Button>
</StackPanel>
<Grid ClipToBounds="True">
<k:GraphPaper x:Name="Paper"></k:GraphPaper>
</Grid>
</DockPanel>
</Window>
然后来写 C# 代码。
首先进行初始化。我们需要显示当前的多边形,和 割角后的多边形,因此我们将他们先初始化出来,默认都设为空。
public partial class Window1 : Window
{
GraphPaper gp = null;
Polygon polygon = new Polygon();
Polygon subPolygon = new Polygon();
bool isSub = false;
bool ready;
public Window1()
{
InitializeComponent();
gp = this.FindName("Paper") as GraphPaper;
InitPoly(polygon, Brushes.Black);
InitPoly(subPolygon, Brushes.Firebrick);
gp.Children.Add(polygon);
gp.Children.Add(subPolygon);
ready = true;
}
private void InitPoly(Polygon p, SolidColorBrush b)
{
p.Stroke = b;
p.StrokeThickness = 0.5;
p.StrokeMiterLimit = 1;
p.Fill = null;
}
我们初始化两个多边形,并将它们设置为不同的颜色,并设置斜接截断值,避免在两边夹角较小时,出现过长的斜接。
接下来是 Clear 按钮的响应事件,我们把 多边形顶点全部清除,并把标识变量设为初始值即可。
private void b2Click(object sender, RoutedEventArgs e)
{
polygon.Points.Clear();
subPolygon.Points.Clear();
isSub = false;
e.Handled = true;
}
点击 Subdivide 按钮的情形则复杂一些:
首先,如果多边形已经被细分,我们要用细分多边形(subPolygon)的顶点来替换 polygon 的顶点。
接下来细分 polygon 并将细分结果放在 subPolygon 中。
细分意味着,对每个顶点,找到它的前一个顶点 和 后一个顶点,并按照 "2/3—1/3"模式组合,求出割角点位置。
private void b1Click(object sender, RoutedEventArgs e)
{
if (isSub)
{
polygon.Points = subPolygon.Points.Clone();
subPolygon.Points = new PointCollection();
}
int n = polygon.Points.Count;
if (n > 0) isSub = true;
for(int i = 0; i < n; ++i)
{
// 多边形是环形结构,即 顶点 0 和 顶点 n - 1 是相连
// 因此要用求余方式计算前后顶点
int nextv = (i + 1) % n;
int lastv = (i + (n - 1)) % n;
double x = (1f / 3f) * polygon.Points[lastv].X +
(2f / 3f) * polygon.Points[i].X;
double y = (1f / 3f) * polygon.Points[lastv].Y +
(2f / 3f) * polygon.Points[i].Y;
subPolygon.Points.Add(new Point(x, y));
x = (1f / 3f) * polygon.Points[nextv].X +
(2f / 3f) * polygon.Points[i].X;
y = (1f / 3f) * polygon.Points[nextv].Y +
(2f / 3f) * polygon.Points[i].Y;
subPolygon.Points.Add(new Point(x, y));
}
e.Handled = true;
}
最后还要处理鼠标点击。当点击鼠标时,除非多边形已经细分完毕,否则我们都必须在多边形中增加一个顶点。我们可以通过 isSub 标识来查看,如果为 false 则加入新顶点。
public MainWindow()
{
...
MouseLeftButtonDown += MouseButtonDownA;
...
}
public void MouseButtonDownA(object sender, RoutedEventArgs e)
{
if (sender != this) return;
var ee = e as MouseButtonEventArgs;
if (!isSub) polygon.Points.Add(ee.GetPosition(gp));
e.Handled = true;
}
练习:
对偶多边形:
using GraphicsBook;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MyWindow
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
GraphPaper gp = null;
Polygon polygon = new Polygon();
Polygon subPolygon = new Polygon();
bool isSub = false;
bool ready;
public MainWindow()
{
InitializeComponent();
gp = this.FindName("Paper") as GraphPaper;
InitPoly(polygon, Brushes.Black);
InitPoly(subPolygon, Brushes.Firebrick);
gp.Children.Add(polygon);
gp.Children.Add(subPolygon);
MouseLeftButtonDown += MouseButtonDownA;
ready = true;
}
private void InitPoly(Polygon p, SolidColorBrush b)
{
p.Stroke = b;
p.StrokeThickness = 0.5;
p.StrokeMiterLimit = 1;
p.Fill = null;
}
public void MouseButtonDownA(object sender, RoutedEventArgs e)
{
if (sender != this) return;
var ee = e as MouseButtonEventArgs;
if (!isSub) polygon.Points.Add(ee.GetPosition(gp));
e.Handled = true;
}
private void b1Click(object sender, RoutedEventArgs e)
{
if (isSub)
{
polygon.Points = subPolygon.Points.Clone();
subPolygon.Points = new PointCollection();
}
int n = polygon.Points.Count;
if (n > 0) isSub = true;
for(int i = 0; i < n; ++i)
{
// 多边形是环形结构,即 顶点 0 和 顶点 n - 1 是相连
// 因此要用求余方式计算前后顶点
int nextv = (i + 1) % n;
int lastv = (i + (n - 1)) % n;
double x = (1f / 2f) * polygon.Points[lastv].X +
(1f / 2f) * polygon.Points[i].X;
double y = (1f / 2f) * polygon.Points[lastv].Y +
(1f / 2f) * polygon.Points[i].Y;
subPolygon.Points.Add(new Point(x, y));
//x = (1f / 2f) * polygon.Points[nextv].X +
// (1f / 2f) * polygon.Points[i].X;
//y = (1f / 2f) * polygon.Points[nextv].Y +
// (1f / 2f) * polygon.Points[i].Y;
//subPolygon.Points.Add(new Point(x, y));
}
e.Handled = true;
}
private void b2Click(object sender, RoutedEventArgs e)
{
polygon.Points.Clear();
subPolygon.Points.Clear();
isSub = false;
e.Handled = true;
}
}
}
结论:一个多边形,对其进行对偶化,其后继的对偶多边形将会逐渐趋于 非自相交