第四章 2D 图形测试平台

第四章 2D 图形测试平台

使用 2D测试平台

在书的官网 cgpp.net 提供了 2D 图形测试平台,我们只需要下载其源码,然后根据需要,将里面的东西放入到自己的 WPF 项目中即可。

割角

打开在官网下载的文件,找到 Subdiv 文件夹,打开里面的 vs项目,并生成 exe 文件。可以先运行一下,体验一下:
在这里插入图片描述
在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oIKXcEc-1601715740984)(en-resource://database/15565:1)]

打开项目,查看 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;
        }
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值