65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 简单拼图

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2010 年 7 月 9 日

CPOL

6分钟阅读

viewsIcon

45250

downloadIcon

3453

在简单的拼图游戏应用程序中开发拖放技术。

引言

本文讨论了一个简单的拼图游戏,该游戏包含 9 块拼图,并使用 Windows Presentation Foundation 实现。本文表明,在 Windows Presentation Foundation 中实现拖放并非难事。

背景

我非常感谢 Rudi Grobler 先生撰写的文章《使用附加属性的 ListBox 拖放》。本应用程序中使用的代码基于他文章中的源代码。我也非常感谢 Seshi 先生撰写的文章《8 拼图 - WPF》,其应用程序 UI 启发了本应用程序的 UI。

Using the Code

本应用程序中的类有

  • 谜题
  • 属性和对象

    1. puzzlePiece:一个表示拼图块的集合。
    2. 名称

    方法

    • Puzzle:构造函数。
    • OnEdit:引发事件的方法。
    • Initialize:加载所有拼图块并将其打乱顺序的方法。
    • Validate:验证拼图的方法。

    事件

    • Edited:在拼图的排列被编辑时为每个拼图引发的事件。
  • PuzzlePiece
  • 属性和对象

    1. index:拼图块的索引。
    2. PuzzleImageSource
    3. UriString
    4. DragFrom:指示拼图块是从 ListBox/Canvas 拖动的。
  • MainWindow(UI)
  • 属性和对象

    1. puzzle:表示此应用程序中使用的拼图。拼图本身由一个 puzzlePiece 组成,它属于 ObservableCollection 类型,表示必须排列好的拼图块;name,即拼图名称;以及一个 Edited 事件,该事件在拼图被编辑时触发。
    2. itemPlacement:表示拼图块的放置位置。它用于验证拼图。它是一个拼图块的集合,其索引代表 Canvas 的索引,并且它包含被拖放到 Canvas 本身的拼图块。
    3. emptyItem:表示一个空的拼图项,用于指示 Canvas 是否不包含拼图项。
    4. lbDragSource:一个 ListBox 对象,用于引用引发拖动的 ListBox
    5. cvDragSource:一个 Canvas 对象,用于引用引发拖动的 Canvas

    方法

    • MainWindow():构造函数。
    • puzzleItemList_PreviewMouseLeftButtonDown:处理拖动到 ListBox 的尝试。
    • PzItmCvs_MouseLeftButtonDown:处理拖动到 Canvas 的尝试。
    • PuzzleItemDrop:处理拖放到 Canvas 的情况。
    • puzzle_Edited:处理拼图的编辑事件。
    • GetDataFromCanvas:如果拖动来自 Canvas,则获取将通过拖放操作传输的数据。
    • GetObjectDataFromPoint:如果拖动来自 ListBox,则获取将通过拖放操作传输的数据。
    • instruction_Click

应用程序场景

应用程序的步骤如下:

  1. 拼图块被加载到列表框中。
  2. 玩家将拼图块从左侧的 ListBox 拖到右侧的九个 Canvas 中的一个。玩家也可以将一个拼图块从一个 Canvas 拖到另一个 Canvas。如果目标 Canvas 为空,则拼图块将被移动。如果不为空,则拼图块将被交换(参见截图)。
  3. 每次编辑拼图时,都会引发一个事件,然后验证拼图。如果拼图排列正确,玩家将获胜。

屏幕截图

该应用程序使用非常简单的用户界面,如下图所示。在左侧,有一个 ListBox,其中包含拼图块。拼图块将被拖放到右侧的 Canvas 之一。通过将其拖放到另一个 Canvas 中的拼图块,拼图块也可以与其他拼图块交换。

wpfsimplepuzzle.png

初始化

首先,加载拼图块。此应用程序可以使用一种以上的拼图。传递给 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 的项源设置为拼图块,并定义编辑处理程序。

拖动

在此应用程序中有两个元素可以拖动:ListBoxCanvas。它们都使用 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 类型的对象。

CanvasPreviewMouseLeftButtonDown 是相同的,只有一些小的修改,用于从 Canvas 获取数据并执行拖放操作。不同之处在于,在 Canvas 中,我们直接从 itemPlacement 获取数据,因为在 Canvas 控件中,只有图像,而不是整个 PuzzlePiece 对象。

放置

放置有四种可能的情况:

  1. 如果 Canvas 为空,并且拼图块是从 ListBox 拖动的,则拼图块将被放置。
  2. 如果 Canvas 为空,并且拼图块是从另一个 Canvas 拖动的,则拼图项将被移动到那里。
  3. 如果 Canvas 不为空,并且拼图块是从 ListBox 拖动的,则拼图项不会被放置。
  4. 如果 Canvas 不为空(有另一个拼图块),并且拼图块是从另一个 Canvas 拖动的,则这两个拼图块将被交换。

当拼图块被放置时,有几件事要做:将其放置在合适的 Canvas 中,更新拼图项的放置,如果不是交换,则从源(ListBox / Canvas)中删除该项,检查拼图是否有效,并删除 DragFrom 属性的值。对于情况 1 和 2,过程很简单;首先,我们检查 CanvasChildren 属性是否为 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)。

历史

这是拼图应用程序的第一个版本。

© . All rights reserved.