用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这个等待返回结果的家伙。

浙公网安备 33010602011771号