程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WPF自定義控件 - 復合控件(中國象棋聯機版)

WPF自定義控件 - 復合控件(中國象棋聯機版)

編輯:關於.NET

對於自己做的游戲即使輸了臉上一般還是洋溢著笑容的,那麼如何做一個簡 單的游戲呢?(.netFramework 3.5 SP1)

一.再說呈現

在自繪那篇我們已經知道了如何把東西畫出來, 或許你已經能通過重載OnRender函數非常熟練的畫出這樣棋子。

那麼OnRender方法中的DrawingContext參數到底來自哪呢?因為 DrawingContext是抽象類,所以微軟創建了一個叫做RenderDataDrawingContext 的具體類以及他的子類VisualDrawingContext,我們的所用的DrawingContext實 際就是VisualDrawingContext這個類,不過微軟都把他們定義為了internal,我 們在程序集以外無法訪問,既然無法訪問,那麼當我們需要多個這樣的對象時如 何創建呢?

DrawingVisual這類為我們實現多個DrawingContext成為了可 能,因為他的實例方法RenderOpen()在內部創建了VisualDrawingContext, DrawingVisual也可以說繼承於Visual,當我們用DrawingContext的Drawing一些 東西的時候,其實產生的是畫圖的數據,數據有了,可要把數據給UI 的線程才 能被顯示,WPF似乎是用ContextLayoutManager這個類來把UI重繪請求放到 Dispatcher隊列,用Visual裡的DUCE發送消息和線程對話.

我們創建可視 數據的代碼可以寫成這樣:

DrawingVisual boardVisual =  new DrawingVisual();
using (DrawingContext drawingContext =  boardVisual.RenderOpen())
{
//畫棋盤
}

以 上是創建了畫圖的數據,那麼怎麼用ContextLayoutManager把數據給Dispatcher 隊列。UIElement中的 PropagateResumeLayout方法循環遞歸把需要刷新的 Visual對象放到隊列中,經過這樣的分裝我們只需要知道把需要呈現的Visual仍 給系統就可以,他自己會判斷是否要刷新。

怎麼給Visual,微軟要求我 們先給Visual的數量,這需要我們通過以下方式來給定

protected  override int VisualChildrenCount
{
get
{
return 1;
}
}

然後他會用一個for循環來得到需 要的Visual對象

for (int i = 0; i <  internalVisualChildrenCount; i++)
{
Visual visualChild =  v.InternalGetVisualChild(i);
if (visualChild != null)
{
PropagateResumeLayout(v, visualChild);
}
}

internalVisualChildrenCount的數量就是VisualChildrenCount 返回的值, InternalGetVisualChild的方法實際做的就是我們常重載的 GetVisualChild方法。

protected override Visual  GetVisualChild(int index)
{
if (index == 0)
return  boardVisual;
else
throw new ArgumentOutOfRangeException ("out of range");
}

就這麼簡單,我們是否 已經看到自己畫的棋盤了。

二.可視樹與邏輯樹

雖然我也承認這 兩棵樹已經被人刨根問底的掘了N次,但對於自定義控件來說這樹可不是說捨棄 就捨棄的(要充分合理利用資源嘛)。

邏輯樹(Logical Tree)當然是邏 輯意義上的,如果把Visual 可以比作汽車的一部分比如車廂,輪胎,油箱,當 我們坐在車廂裡的時候我們實際也可以說做在車中,車是一個邏輯意義上的含義 ,是各個汽車零件和的總稱。所以我們的控件上的Parent或者是Child一般都是 邏輯的物件,那麼加入他除了一個標示外,還有其他的什麼意義呢?他還可以屬 性值繼承,比如說我們在我們這個象棋控件上設置下字體的變化,希望上面的棋 子車、馬、帥等的字體也發生變化就可以用到他。

在我們的象棋控件中 這樣注冊:

public static readonly DependencyProperty  FontFamilyProperty =
DependencyProperty.Register ("FontFamily",
typeof(FontFamily),
typeof (ChineseChessboard),
new FrameworkPropertyMetadata (SystemFonts.MessageFontFamily,  FrameworkPropertyMetadataOptions.Inherits));

棋子中可以這 樣

public static readonly DependencyProperty  FontFamilyProperty = ChineseChessboard.FontFamilyProperty.AddOwner (typeof(ChessmanControl),
new FrameworkPropertyMetadata (SystemFonts.MessageFontFamily,  FrameworkPropertyMetadataOptions.Inherits));

另外的還有事 件路由和資源就不多說了(我也沒用到^-^)。

可視樹(Visual Tree)據 說是邏輯樹的擴展,所以把元素只加入可視樹依然可以進行屬性繼承,當然前提 是類要繼承於FrameworkElement 或FrameworkContentElement,從樹的字面意義 來看似乎告訴我們不加入他就不能看到,實際上他和呈現沒有太大關系,可問題 是當你把一個visual顯示出來了,也停留在只可遠觀而不可亵玩的地步 —— 不能引發事件,加入了VisualTree 這個問題就可以解決了,有 了點擊測試(HitTest),事件也有了,人也順心了,編程也不困惑了。這裡要說明 下當我把整個大棋盤加入到可視樹時,上面的棋子未加入到可視樹的前提下,棋 子上的鼠標響應依然可以獲得HitTest也能捕捉的到棋子,可見WPF似乎根據一個 區域的像素點來判斷。至於VisualTreeHelper裡的方法可以自己查詢MSDN了解。

說到這兩個樹還有兩個類值得提下:VisualCollection和 UIElementCollection 前者可以用來操作可視樹,後者既可以操作可視樹又可以 操作邏輯樹,如果只想操作可視樹,可以把UIElementCollection 構造函數中的 logicParent 賦為null。使用了這兩個類你就可以簡單操作樹上的元素了。

三.HitTest和TranslateTransform

當我們把棋子放到 UIElementCollection 中,並輸出隊列

protected override  int VisualChildrenCount
{
get
{
// +1 是為棋盤
return (ChessmanCollection != null ?  ChessmanCollection.Count : 0) + 1;
}
}
protected  override Visual GetVisualChild(int index)
{
//第一個為棋 盤其他為棋子
if (index == 0)
return boardVisual;
return ChessmanCollection[index - 1];
}

並輸出 他們的實際大小

protected override Size MeasureOverride (Size availableSize)
{
for (int i = 0;  ChessmanCollection != null && i <  ChessmanCollection.Count; i++)
{
ChessmanCollection [i].Measure(availableSize);
}
return new Size(CellWidth *  10, CellWidth * 11);
}

看到了棋子的顯示,我們接 下來的事便是要選中棋子

protected override void  OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
Point  location = e.GetPosition(this);
HitTestResult result =  VisualTreeHelper.HitTest(this, location);
ChessmanControl  chessmanControl = result.VisualHit as  ChessmanControl;

當然點擊測試還可以做很多事,具體的看 http://msdn.microsoft.com/zh-cn/library/ms752097.aspx

選中以後我 們要移動,並需要一個移動過程的效果

TranslateTransform  _moveTransform = new TranslateTransform();
_chessmanBaseDic [newChessman].RenderTransform = _moveTransform;
DoubleAnimation  xAnimation = new DoubleAnimation((newPoint.X - oldPoint.X) *  CellWidth, _moveDuration, FillBehavior.Stop);
_moveTransform.BeginAnimation(TranslateTransform.XProperty,  xAnimation);
DoubleAnimation yAnimation = new  DoubleAnimation((newPoint.Y - oldPoint.Y) * CellWidth,  _moveDuration, FillBehavior.Stop);
EventHandler tempAction =  default(EventHandler);
tempAction = delegate
{
_chessmanBaseDic[newChessman].ClearValue (UIElement.RenderTransformProperty);
_chessmanBaseDic [newChessman].isSelected = false;
_currentChessmanControl =  null;
if (oldChessman != null)
{
ChessmanCollection.Remove(_chessmanBaseDic[oldChessman]);
}
//更新
this.InvalidateArrange();
//移除本身
yAnimation.Completed -= tempAction;
};
yAnimation.Completed += tempAction;
_moveTransform.BeginAnimation(TranslateTransform.YProperty,  yAnimation);

如果你感覺寫兩個Animation來分別控制有點蠢的 話,你也可以把TranslateTransform 放到CompositionTarget.Rendering來控制 。

當然這樣移動是暫時的,所以我們要引發InvalidateArrange()來實際 輸出控件的位置

protected override Size ArrangeOverride (Size finalSize)
{
for (int i = 0; ChessmanCollection  != null && i < ChessmanCollection.Count; i++)
{
ChessmanControl item = ChessmanCollection[i] as  ChessmanControl;
item.Arrange(new Rect(
new Point (item.Chessman.Location.X * CellWidth - item.DesiredSize.Width /  2+CellWidth,
item.Chessman.Location.Y * CellWidth -  item.DesiredSize.Height / 2+CellWidth),
item.DesiredSize));
}
return this.DesiredSize;
}

四.畫圖問題

棋盤背景可以加張圖片

BitmapImage backgroundImage  = new BitmapImage();
backgroundImage.BeginInit();
backgroundImage.UriSource = new Uri (@"pack://application:,,,/ChineseChessControl;component/Images/wo odDeskground.jpg", UriKind.RelativeOrAbsolute);
backgroundImage.EndInit();
backgroundImage.Freeze ();

其中的Uri 如果不熟悉的話可以看下面兩個網址(一個為3.5 的一個為3.0的):

http://msdn.microsoft.com/en- us/library/aa970069.aspx#The_Pack_URI_Scheme

http://msdn.microso ft.com/en-us/library/aa970069(VS.85).aspx

對於棋盤的立體效果,是 用兩條線來達到的,上面一條用了深色畫筆,下面一個用了淺色畫筆

drawingContext.DrawLine(darkPen, new Point(CellWidth *  i, CellWidth), new Point(CellWidth * i, CellWidth * 10 /  2));
drawingContext.DrawLine(lightPen, new Point(CellWidth *  i + 1.5, CellWidth), new Point(CellWidth * i + 1.5,  CellWidth * 10 / 2));

棋盤的話也可以先畫四分之一,然 後通過反轉得到

//第一象限
drawingContext.PushTransform(new  ScaleTransform(-1, 1, CellWidth * 5, CellWidth * 5.5));
quarterBoard();
drawingContext.Pop();
//第二象限
quarterBoard();
//第三象限
drawingContext.PushTransform (new ScaleTransform(1, -1, CellWidth * 5, CellWidth *  5.5));
quarterBoard();
drawingContext.Pop();
//第四象限
drawingContext.PushTransform(new ScaleTransform(-1, -1,  CellWidth * 5, CellWidth * 5.5));
quarterBoard();
drawingContext.Pop();

對於楚河、漢界幾個字先文字豎排, 文字豎排其實就是限制文字輸出的寬度,讓他把字給擠下去。

FormattedText textFormat = new FormattedText(
text, System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface ("STLiti"),
fontSize,
Brushes.Black);
textFormat.MaxTextWidth = fontSize;
textFormat.TextAlignment  = TextAlignment.Justify;

在翻轉的時候要注意的是我們的 翻轉的文字塊往往是長方形,而不是正方形,

所以你在需要先把A點的X坐標加上長寬之差一半的距離再加全部字寬 度的一半,Y坐標減去長寬之差的一半加上字體大小的一半再加全部字的高度的 一半

五.動畫提醒

有些時候下棋的時候會沒有覺察出對方下了哪 個棋,應該過幾秒提醒下,順便也督促下已經思考了一段時間了快快下吧。

我們可以使用 DispatcherTimer類來計時,讓其每個幾秒來做提醒。

DispatcherTimer _dispatcherTimer = new  DispatcherTimer(DispatcherPriority.ApplicationIdle);
_dispatcherTimer.Interval = TimeSpan.FromSeconds(10);
_dispatcherTimer.Tick += delegate
{
_renderingListener.StartListening();
};

然後用CompositionTarget.Rendering來改變控件的透明度達到 跳動的效果。

_renderingListener.Rendering += delegate
{
if (tempChessmanControl == null)
tempChessmanControl  = _lastMoveChessman;
if (tempChessmanControl != null)
{
if (frameCount % 20 == 0)
{
tempChessmanControl.Opacity = tempChessmanControl.Opacity > 0  ? 0 : 1;
}
frameCount++;
if (tempChessmanControl  != _lastMoveChessman //已經換了棋子
|| frameCount > 120  //提醒大於規定
|| !IsRedReady   //紅方沒有開始,或已經結束
|| !IsBlueReady)  //藍方沒有開始,或已經結束
{
tempChessmanControl.Opacity = 1;
frameCount = 0;
tempChessmanControl = null;
_renderingListener.StopListening ();
}
}
};

六.聯機操作

協議: UDP

先發送廣播給局域網看是否有空閒主機。

空閒主機接到廣播 如果空閒則回應。

創建主機的時候再發廣播告訴已經有新建主機。

互聯後告訴其他主機已經在對戰,讓其從服務列表下拿掉。

走一 步告訴對方是從哪個坐標點移動到哪個坐標點

沒做

發送廣播

public void SendBroadcast(object obj, int port)
{
byte[] sendbuf = UDPClass.Serialization(obj);
Socket  s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram,  ProtocolType.Udp);
IPEndPoint ep = new IPEndPoint (IPAddress.Broadcast, port);
s.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
s.SendTo(sendbuf, ep);
s.Close();
}

發送普通 消息

public void Send(IPAddress ipAddress, object  obj, int port)
{
byte[] sendbuf =  UDPClass.Serialization(obj);
Socket s = new Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint ep = new IPEndPoint(ipAddress, port);
s.SendTo (sendbuf, ep);
s.Close();
}

監聽端口

private void StartUdpListenerPort()
{
bool  done = true;
using (UdpClient listener = new UdpClient())
{
listener.EnableBroadcast = true;
IPEndPoint iep  = new IPEndPoint(IPAddress.Any, _listenPort);
//端口復用
listener.Client.SetSocketOption(SocketOptionLevel.Socket,  SocketOptionName.ReuseAddress, true);
listener.Client.SetSocketOption(SocketOptionLevel.Socket,  SocketOptionName.ExclusiveAddressUse, false);
listener.Client.Bind(iep);//綁定這個實例
try
{
while  (done)
{
byte[] bytes = listener.Receive(ref iep);
_callback(iep,_listenPort, UDPClass.Deserialize(bytes));
}
}
finally
{
listener.Close();
}
}
}

因為存在多客戶端所以一個端口不能重用,我是從2000可以,每 次加一看看是否被使用過。這裡用了比較爛的方法,不知道大家有沒有比較好的 辦法

public static int GetIdlePort(int startPort)
{
while (true)
{
Socket s = new Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint ipport = new IPEndPoint(IPAddress.Any,  startPort);
try
{
s.Bind(ipport);
break;
}
catch
{
startPort++;
}
finally
{
s.Close();
}
}
return startPort;
}

七.窗體關閉事件

窗體的事件是在View上的,如何讓 ViewModel和之上的事件相綁定呢?我們知道事件其實包含的是一個委托的集合 ,如果讓View上的事件所用的委托同ViewModel的委托相綁定不就得的效果了。 我們可以參考Prism框架做個中間件,用附加屬性給窗體以作綁定。

<Window x:Class="ChineseChess.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentatio n"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ChineseChess"
xmlns:views="clr-namespace:ChineseChess.Views"
Width="500" Height="600">
<local:WindowRegionMetadata.WindowRegionMetadata>
<local:WindowRegionMetadata BeforeClose="{Binding  PersistAction}"/>
</local:WindowRegionMetadata.WindowRegionMetadata>
<Window.Title>
<MultiBinding StringFormat="{} {0}:{1}">
<Binding Path="LocalIP"/>
<Binding Path="Port"/>
</MultiBinding>
</Window.Title>

但是 App中的啟動要改成這樣

protected override void  OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Shell window = WindowRegionBehavior.CreateWindow<Shell> (null, null);
ShellViewModel shellViewModel = new  ShellViewModel();
window.DataContext = shellViewModel;
window.Show();
}

具體的代碼限於篇幅 請參見事例。

八.寫在最後

本事例只是簡單的實現了一些功能, 如有需要添加的可以自己練練手,做些小游戲還是很能提高對程序的積極性的。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved