用XNA4.0编写一个【微观模型演示】涉及到的一些技巧

本来是为了评中教一级编写的一个课件,用来编辑和演示3D化学分子模型,刚刚上报还不知道结果如何呢。想起设计时用到到一些技术可能给初学XNA的朋友一些帮助。于是写了这篇文章。如果想学XNA4.0可以去网上找找《XNA+4.0+学习指南》很好的一本书,示例代码是用C#写的,不过各种语言看多了,代码具体实现就没有那么重要,关键是XNA的用法和一些技巧的思路。好了,不罗嗦了。

继续先贴图:

涉及到的几个技巧主要是

1、克隆

2、撤销、恢复

3、界面两侧的panel

逐个说一下:

1、克隆

要复制一个类实例,方法有很多,比较笨拙的是再new一个,并且把各个属性都复制一份……我所使用的方法就是这个笨拙的方法……稍微改进一下:

    Public Function Clone() As BasicModel
        '序列化
        Dim ts() As Type = {GetType(BasicModel), GetType(Sphere), GetType(Cylinder)}
        Dim mySerializer As XmlSerializer = New XmlSerializer(GetType(BasicModel), ts)
        Dim myWriter As MemoryStream = New MemoryStream
        mySerializer.Serialize(myWriter, Me)
        myWriter.Position = 0
        '反序列化
        Dim ret As BasicModel = CType(mySerializer.Deserialize(myWriter), BasicModel)
        myWriter.Close()
        '重加载模型
        If Type = ModelType.Sphere Then
            ret.model = ModelManager.LoadSphereModel
        ElseIf Type = ModelType.Cylinder Then
            ret.model = ModelManager.LoadCylinderModel(CType(ret, Cylinder).Number - 1)
        End If
        Return ret
    End Function

这个就是模型基类提供的克隆方法,为了提高克隆速度,使用了XmlSerializer解析克隆对象,序列化到内存流,然后反序列化。如果用二进制序列化和反序列化反射过程将非常耗时。代码中有这么几句:

        '重加载模型
        If Type = ModelType.Sphere Then
            ret.model = ModelManager.LoadSphereModel
        ElseIf Type = ModelType.Cylinder Then
            ret.model = ModelManager.LoadCylinderModel(CType(ret, Cylinder).Number - 1)
        End If

这里面的model就是xna里面的model,它是不能直接序列化的,当然也有人提出可以继承这个类,标记为可序列化。但是在这个程序的应用中,由于只有几种模型(球一个,单键一个,双键一个,三键一个,一共四个模型),所以很好分辨,于是我就直接“重新加载”了一下用一下代码加载过的模型:

        modelManager.LoadSphereModel = Content.Load(Of Model)("models\Sphere")
        For i As Integer = 0 To 2
            modelManager.LoadCylinderModel(i) = Content.Load(Of Model)("models\Cylinder" & (i + 1))
        Next

所以这种方式来看,减少了序列化对象和编码,也减少了重复读取文件的过程。这就是第一个技巧了。

 

2、撤销、恢复

作为一个编辑软件,如果没有重复和撤销功能,那将是使用者非常痛苦的事情;编写游戏的话,那恐怕会被OOXXOXOXOOOXXX吧……。其实这个功能也很简单:

所有的模型操作都被模型本身的worldmatrix记录着,所以只需要克隆一份modelmanager中保存模型的数组就可以了。具体的代码是这样写的,可能有些冗长:

 

    Public ToBack, ToNext As Boolean
    Dim StepSave As New List(Of List(Of BasicModel))
    Dim StepIndex As Integer = -1
    '获取下一个点
    Public Sub StepListNext()
        Dim nextsetp As Integer = StepIndex + 1
        If nextsetp > StepSave.Count - 1 Then Return
        StepIndex = nextsetp
        ToNext = StepIndex < StepSave.Count - 1
        ToBack = StepIndex > 0
        Models.Clear()
        Models = Nothing
        Models = CloneListOfBasicModel(StepSave(StepIndex))
        rotation3OfBaseModels = Models(0).RotationWorld3
        BoudingSphereRenderer.show = False
    End Sub
    '获取上一个点
    Public Sub StepListBack()
        Dim backsetp As Integer = StepIndex - 1
        If backsetp < 0 Then
            Return
        End If
        StepIndex = backsetp
        ToNext = StepIndex < StepSave.Count - 1
        ToBack = StepIndex > 0
        Models.Clear()
        Models = Nothing
        Models = CloneListOfBasicModel(StepSave(StepIndex))
        rotation3OfBaseModels = Models(0).RotationWorld3
        BoudingSphereRenderer.show = False
    End Sub
    '清理保存数组
    Public Sub StepListClear()
        For Each msetp As List(Of BasicModel) In StepSave
            msetp.Clear()
            msetp = Nothing
        Next
        StepSave.Clear()
        StepIndex = -1
        ToBack = False
        ToNext = False
        rotation3OfBaseModels = Matrix.Identity
        For Each m As BasicModel In Models
            m.RotationWorld3 = rotation3OfBaseModels
        Next
    End Sub
    '克隆全部模型保存在数组
    Public Sub StepListSave()
        If StepSave.Count - 1 > StepIndex Then
            StepSave.RemoveRange(StepIndex + 1, StepSave.Count - StepIndex - 1)
        End If
        StepSave.Add(CloneListOfBasicModel(Models))
        StepIndex = StepSave.Count - 1
        ToNext = StepIndex < StepSave.Count - 1
        ToBack = StepIndex > 0
    End Sub

    Private Function CloneListOfBasicModel(list As List(Of BasicModel)) As List(Of BasicModel)
        '序列化
        Dim t() As Type = {GetType(BasicModel), GetType(Cylinder), GetType(Sphere), GetType(List(Of BasicModel))}
        Dim mySerializer As Xml.Serialization.XmlSerializer = New Xml.Serialization.XmlSerializer(GetType(List(Of BasicModel)), t)
        Dim myWriter As MemoryStream = New MemoryStream
        mySerializer.Serialize(myWriter, list)
        myWriter.Position = 0
        '反序列化
        Dim ret As List(Of BasicModel) = CType(mySerializer.Deserialize(myWriter), List(Of BasicModel))
        myWriter.Close()
        For i As Integer = 0 To ret.Count - 1
            If ret(i).Type = BasicModel.ModelType.Cylinder Then
                ret(i).model = LoadCylinderModel(CType(ret(i), Cylinder).Number - 1)
            ElseIf ret(i).Type = BasicModel.ModelType.Sphere Then
                ret(i).model = LoadSphereModel
            End If
        Next
        Return ret
    End Function

 

由于在模型处理时,使用了一个始终作用于world矩阵之后的旋转矩阵,所以代码中出现了rotation3OfBaseModels这个变量。之所以这么做,是因为我想要旋转整个模型,当然可以用旋转摄影机的方法,不过由于需要一些特殊操作,所以采用了这种方式。

 

3、使用gdi+控件

其实这个很简单的,因为xna的窗体也是窗体嘛,所以可以用设置新父的方法,把控件从一个windows窗体上移动到xna窗体上(但是一全屏,就不显示咯,需要另外处理)。

在game1的new里面:

xnafrm = CType(System.Windows.Forms.Form.FromHandle(Me.Window.Handle), System.Windows.Forms.Form)

然后在LoadContent里面(不一定的,只是这些控件初始化时用到了一些模型,所以才写到模型加载之后):

        winfrm = New UIHelper
        winfrm.ShowWinfrmUI(graphics.PreferredBackBufferHeight, graphics.PreferredBackBufferWidth, xnafrm, Drawing.Color.CornflowerBlue)

在UIHelper窗体里的ShowWinfrmUI是这么写的:

 

    'ui控制
    Public Sub ShowWinfrmUI(h As Integer, w As Integer, p As Form, c As Color)
        '添加消息过滤器,这里只用来屏蔽,因为有些按键(如backspace)将会引起控件响应,即使没有发送按键消息。所以当发送消息后,导致按键次数多出来一次。
        Application.AddMessageFilter(Me)
        '从枚举中获取数据源初始化下拉列表框
        tscbSphereType.ComboBox.DataSource = [Enum].GetNames(GetType(Sphere.Name))
        tscbCylinderType.ComboBox.DataSource = [Enum].GetNames(GetType(Cylinder.BondType))
        '初始化左右控件容器
        InitializeControl(PanelLeft, h, CInt(IIf(PanelLeft.Height > h, PanelLeft.Width + ScrollBarWidth, PanelLeft.Width)), New Point(1, 0), c, p)
        Dim width As Integer
        If panelRight.Height > h Then width = panelRight.Width + ScrollBarWidth Else width = panelRight.Width
        InitializeControl(panelRight, h, width, New System.Drawing.Point(w - width - 1, 0), c, p)
        '计算未被控件容器占据的空间
        xnaSpaceRect = New XnaRect(PanelLeft.Width, 0, panelRight.Left - PanelLeft.Width, h)
        '设置被按下的按钮
        tsbSelectModel.Checked = True
        '
        tsbStepBack.Enabled = False
        tsbStepNext.Enabled = False
    End Sub

    Private Sub InitializeControl(s As Panel, h As Integer, w As Integer, l As Point, c As Color, p As Form)
        s.Width = w
        s.Height = h
        s.Location = l
        s.AutoScroll = True
        s.HorizontalScroll.Enabled = False
        s.BorderStyle = BorderStyle.FixedSingle
        s.BackColor = c
        '获取子控件,这是一个递归函数,可以遍历到指定控件的全部子控件
        GetChildCtrl(s)
        s.Parent = p
    End Sub

    '识别全部控件中的textbox,从而添加事件处理
    Private Sub GetChildCtrl(ParentCtrl As Control)
        For Each ctrlC As Control In ParentCtrl.Controls
            '设置颜色
            ctrlC.BackColor = ParentCtrl.BackColor
            '如果是toolstrip,那么枚举其子项目中的ToolStripTextBox并添加事件处理程序
            If TypeOf ctrlC Is ToolStrip Then
                For Each item As ToolStripItem In CType(ctrlC, ToolStrip).Items
                    If TypeOf item Is ToolStripTextBox Then
                        '获得和失去焦点,用于识别当前输入焦点所在的ToolStripTextBox,以便向其发送按键消息
                        AddHandler CType(item, ToolStripTextBox).GotFocus, AddressOf txtBox_GotFocus
                        AddHandler CType(item, ToolStripTextBox).LostFocus, AddressOf txtBox_LostFocus
                        '统一的按键处理函数
                        AddHandler CType(item, ToolStripTextBox).KeyUp, AddressOf txtBox_KeyUp
                    End If
                Next
            End If
            '递归
            If ctrlC.HasChildren Then GetChildCtrl(ctrlC)
        Next
    End Sub
    '消息过滤器,只处理按键抬起消息
    Public Function PreFilterMessage(ByRef m As System.Windows.Forms.Message) As Boolean Implements System.Windows.Forms.IMessageFilter.PreFilterMessage
        Select Case m.Msg
            Case WM_KeyUP   '如果是键抬起消息。利用锁来处理。从而过滤掉多出来的消息。(当发送消息时前开始吃掉消息,但消息发送后,就不吃掉了。详见具体发送过程注释。)
                Return IsSendMsg
            Case WM_KEYDOWN '如果是按键按下,吃掉。
                Return True
        End Select
        Return False
    End Function

 

这里的设置也很简单,只要知道原理就好办了。里面之所以用到一个递归,是因为打开菜单那里的快捷按钮里面要显示的弹出菜单要按照data目录中的目录结构形成。

 

重要的是:在程序界面上有好几个工具栏文本框,由于在xna影响下,所以按键需要处理一下,不然输入不了。具体的按键处理函数如下:


    '按键处理函数,该函数接收来自update的按键,并加以处理
    Public Sub Key_Up(e As Microsoft.Xna.Framework.Input.Keys)
        'Debug.Print(e.ToString)
        '当焦点位于textbox、输入字符为0-9或为退格等时
        If (FocusTextBox IsNot Nothing) AndAlso KeysList.Contains(e) Then
            '若textbox中已经有小数点,就退出
            If (e = Input.Keys.Decimal OrElse e = Input.Keys.OemPeriod) AndAlso (FocusTextBox.Text.Contains(".")) Then
                FocusTextBox.Text.Replace(".", String.Empty)
            End If
            '发送消息前,设定发送标记,从而使消息过滤器开始吃掉消息,
            '发送开始,继续屏蔽消息,而当消息到达时,已经处于不屏蔽状态了。
            '于是过滤掉了多余的消息。这也是此处使用postmessage而不是sendmessage的理由。
            '因为sendmessage是同步的, 必须执行完才执行下面的语句,而postmessage是异步的,消息还没到达,下面的语句已经执行了。
            IsSendMsg = True
            PostMessage(FocusTextBox.Control.Handle, WM_KeyUP, New IntPtr(e), New IntPtr(MapVirtualKey(e, WM_KeyUP)))
            IsSendMsg = False
        End If
    End Sub

其中主要就是用PostMessage这个异步处理,而不是使用SendMessage这个等待返回结果的家伙。

posted @ 2012-05-07 16:19  zcsor~流浪dè风  Views(474)  Comments(1)    收藏  举报