第四章 2D 图形测试平台

    科技2022-07-12  125

    第四章 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; } } }

    结论:一个多边形,对其进行对偶化,其后继的对偶多边形将会逐渐趋于 非自相交

    Processed: 0.011, SQL: 8