WPF 简单拼图
在简单的拼图游戏应用程序中开发拖放技术。
引言
本文讨论了一个简单的拼图游戏,该游戏包含 9 块拼图,并使用 Windows Presentation Foundation 实现。本文表明,在 Windows Presentation Foundation 中实现拖放并非难事。
背景
我非常感谢 Rudi Grobler 先生撰写的文章《使用附加属性的 ListBox 拖放》。本应用程序中使用的代码基于他文章中的源代码。我也非常感谢 Seshi 先生撰写的文章《8 拼图 - WPF》,其应用程序 UI 启发了本应用程序的 UI。
Using the Code
本应用程序中的类有
谜题
puzzlePiece
:一个表示拼图块的集合。名称
Puzzle
:构造函数。OnEdit
:引发事件的方法。Initialize
:加载所有拼图块并将其打乱顺序的方法。Validate
:验证拼图的方法。Edited
:在拼图的排列被编辑时为每个拼图引发的事件。PuzzlePiece
index
:拼图块的索引。PuzzleImageSource
UriString
DragFrom
:指示拼图块是从ListBox
/Canvas
拖动的。MainWindow
(UI)puzzle
:表示此应用程序中使用的拼图。拼图本身由一个puzzlePiece
组成,它属于ObservableCollection
类型,表示必须排列好的拼图块;name
,即拼图名称;以及一个Edited
事件,该事件在拼图被编辑时触发。itemPlacement
:表示拼图块的放置位置。它用于验证拼图。它是一个拼图块的集合,其索引代表Canvas
的索引,并且它包含被拖放到Canvas
本身的拼图块。emptyItem
:表示一个空的拼图项,用于指示Canvas
是否不包含拼图项。lbDragSource
:一个ListBox
对象,用于引用引发拖动的ListBox
。cvDragSource
:一个Canvas
对象,用于引用引发拖动的Canvas
。MainWindow()
:构造函数。puzzleItemList_PreviewMouseLeftButtonDown
:处理拖动到ListBox
的尝试。PzItmCvs_MouseLeftButtonDown
:处理拖动到Canvas
的尝试。PuzzleItemDrop
:处理拖放到Canvas
的情况。puzzle_Edited
:处理拼图的编辑事件。GetDataFromCanvas
:如果拖动来自Canvas
,则获取将通过拖放操作传输的数据。GetObjectDataFromPoint
:如果拖动来自ListBox
,则获取将通过拖放操作传输的数据。instruction_Click
属性和对象
方法
事件
属性和对象
属性和对象
方法
应用程序场景
应用程序的步骤如下:
- 拼图块被加载到列表框中。
- 玩家将拼图块从左侧的
ListBox
拖到右侧的九个Canvas
中的一个。玩家也可以将一个拼图块从一个Canvas
拖到另一个Canvas
。如果目标Canvas
为空,则拼图块将被移动。如果不为空,则拼图块将被交换(参见截图)。 - 每次编辑拼图时,都会引发一个事件,然后验证拼图。如果拼图排列正确,玩家将获胜。
屏幕截图
该应用程序使用非常简单的用户界面,如下图所示。在左侧,有一个 ListBox
,其中包含拼图块。拼图块将被拖放到右侧的 Canvas
之一。通过将其拖放到另一个 Canvas
中的拼图块,拼图块也可以与其他拼图块交换。
初始化
首先,加载拼图块。此应用程序可以使用一种以上的拼图。传递给 Initialize
方法的参数用于确定使用哪种拼图。它可以是随机生成的,但在本应用程序中,只有一个内置拼图,即兔子拼图。因此,将值 1 作为方法调用的参数传递。最初,初始化是按顺序进行的。因此,在初始化之后,我们必须打乱拼图的顺序。这是通过 .NET 生成的随机数实现的。
public void Initialize(int chosen)
{
string directorySource = "";
if (chosen == 1)
{
this.name = "Rabbit Puzzle";
directorySource = "RabbitPuzzle";
}
for(int i=0; i<9; i++)
{
this.puzzlePiece.Add(new PuzzlePiece());
this.puzzlePiece[i].index = i;
this.puzzlePiece[i].UriString =
"Puzzle/" + directorySource + "/" + (i + 1).ToString() + ".png";
this.puzzlePiece[i].PuzzleImageSource =
new BitmapImage(new Uri(this.puzzlePiece[i].UriString, UriKind.Relative));
}
//shuffle
Random rand = new Random();
for (int i = 0; i < 9; i++)
{
int random = rand.Next(0, 8);
PuzzlePiece buffer;
buffer = this.puzzlePiece[i];
this.puzzlePiece[i] = this.puzzlePiece[random];
this.puzzlePiece[random] = buffer;
}
}
MainWindow
类中的 itemPlacement
集合对象用于映射 Canvas
和其中包含的拼图块。默认值将是 emptyItem
,它是在构造函数中定义的具有特定值的对象。然后,将 ListBox
的项源设置为拼图块,并定义编辑处理程序。
拖动
在此应用程序中有两个元素可以拖动:ListBox
和 Canvas
。它们都使用 PreviewMouseLeftButtonDown
。
在列表框的 PreviewMouseLeftButtonDown
中,通过拖放操作传输的数据来自 GetObjectDataFromPoint
方法。此方法使用拖动源和相对于拖动源的鼠标坐标作为参数。那么,GetObjectDataFromPoint
方法里面有什么呢?这是 Rudi Grobler 先生在他的文章《ListBox 拖放使用附加属性》中的方法(参见 https://codeproject.org.cn/KB/WPF/WPFDragDrop.aspx)。我们可以通过以下方式修改该方法来推测其内部工作原理:
private object GetObjectDataFromPoint(ListBox dragSource, Point point)
{
UIElement element = dragSource.InputHitTest(point) as UIElement;
//MessageBox.Show("Drag Source Element : " + element.ToString());
if (element != null)
{
object data = DependencyProperty.UnsetValue;
while (data == DependencyProperty.UnsetValue)
{
data = dragSource.ItemContainerGenerator.ItemFromContainer(element);
if (data == DependencyProperty.UnsetValue)
{
element = VisualTreeHelper.GetParent(element) as UIElement;
//MessageBox.Show("Element passed through : " + element.ToString());
}
if (element == dragSource)
{
return null;
//MessageBox.Show("element == dragSource");
}
}
if (data != DependencyProperty.UnsetValue)
{
//MessageBox.Show("Data : " + data.ToString());
return data;
}
}
return null;
}
只需取消注释 MessageBox.Show(...)
语句并运行应用程序。从被拖动的图像元素开始,通过 while
语句迭代地查找其父级,直到达到 ListBoxItem
。从 ListBoxItem
中,我们通过 ItemFromContainer
方法获取数据。数据是 PuzzlePiece
类型的对象。
Canvas
的 PreviewMouseLeftButtonDown
是相同的,只有一些小的修改,用于从 Canvas
获取数据并执行拖放操作。不同之处在于,在 Canvas
中,我们直接从 itemPlacement
获取数据,因为在 Canvas
控件中,只有图像,而不是整个 PuzzlePiece
对象。
放置
放置有四种可能的情况:
- 如果
Canvas
为空,并且拼图块是从ListBox
拖动的,则拼图块将被放置。 - 如果
Canvas
为空,并且拼图块是从另一个Canvas
拖动的,则拼图项将被移动到那里。 - 如果
Canvas
不为空,并且拼图块是从ListBox
拖动的,则拼图项不会被放置。 - 如果
Canvas
不为空(有另一个拼图块),并且拼图块是从另一个Canvas
拖动的,则这两个拼图块将被交换。
当拼图块被放置时,有几件事要做:将其放置在合适的 Canvas
中,更新拼图项的放置,如果不是交换,则从源(ListBox
/ Canvas
)中删除该项,检查拼图是否有效,并删除 DragFrom
属性的值。对于情况 1 和 2,过程很简单;首先,我们检查 Canvas
的 Children
属性是否为 0(表示没有子元素)。然后,我们定义一个图像控件,其宽度、高度和源是图像源的宽度和高度,以及我们从通过拖动操作传输的拼图块数据中获得的图像源。之后,删除旧的拼图块并更新放置。
Image imageControl = new Image()
{
Width = destination.Width,
Height = destination.Height,
Source = itemTransferred.PuzzleImageSource,
Stretch = Stretch.UniformToFill
};
//For condition 1 a and b, canvas is empty
if (destination.Children.Count == 0)
{
//put the puzzle piece to the canvas
destination.Children.Add(imageControl);
//Step 2
//Update PuzzleItemPlacement
//get the placement index to be updated
int indexToUpdate = int.Parse(destination.Tag.ToString());
//update now
//this statement is for condition 1 a (item from listbox)
if (itemTransferred.DragFrom == typeof(ListBox))
{
//update
this.itemPlacement[indexToUpdate] = itemTransferred;
//Step 3
//delete the item dragged from listbox
//NOTE : DELETING this way makes puzzle
// pieces defined in puzzle.puzzleItem DELETED
((IList)lbDragSource.ItemsSource).Remove(itemTransferred);
}
对于情况 3,我们只需从方法返回即可。对于情况 4,首先,我们必须获取两个索引,即源索引和目标索引。这是因为我们必须交换这两个拼图块,即从被拖动的 Canvas
来的拼图块和拼图块被放置的 Canvas
。要更改 Canvas
中的 Image
控件,可以通过 GetAssociatedCanvasByIndex
方法访问关联的 Canvas
。此方法接受一个整数参数,该参数是要访问的 Canvas
的索引,并返回关联的 Canvas
。
else if (destination.Children.Count > 0)
{
//condition 1c, from listbox
if (itemTransferred.DragFrom == typeof(ListBox))
{
//do nothing
return;
}
//condition 1d
else if (itemTransferred.DragFrom == typeof(Canvas))
{
//Step 1 and 2, switch them
//get the previous and destination index
int sourceIndex = itemPlacement.IndexOf(itemTransferred);
int destinationIndex = int.Parse(destination.Tag.ToString());
Object buffer = null;
//switch the image
Image image0 = new Image() { Width = destination.Width,
Height = destination.Height, Stretch = Stretch.Fill };
image0.Source = itemPlacement[sourceIndex].PuzzleImageSource;
Image image1 = new Image() { Width = destination.Width,
Height = destination.Height, Stretch = Stretch.Fill };
image1.Source = itemPlacement[destinationIndex].PuzzleImageSource;
GetAssociatedCanvasByIndex(sourceIndex).Children.Clear();
GetAssociatedCanvasByIndex(destinationIndex).Children.Clear();
GetAssociatedCanvasByIndex(sourceIndex).Children.Add(image1);
GetAssociatedCanvasByIndex(destinationIndex).Children.Add(image0);
image0 = null;
image1 = null;
//switch the placement
buffer = itemPlacement[sourceIndex];
itemPlacement[sourceIndex] = itemPlacement[destinationIndex];
itemPlacement[destinationIndex] = buffer as PuzzlePiece;
buffer = null;
}
}
结论
欢迎提出更正和建议。我希望本文能帮助到那些在拖放拼图方面遇到困难的人。对于本文中的任何错误,我深感抱歉,非常感谢。
关注点
创建此应用程序的困难之处在于:理解 GetObjectDataFromPoint
方法,以及手动使用 Microsoft Paint 切割兔子图像(相信我,这很难 :D)。
历史
这是拼图应用程序的第一个版本。