Introduction
This article discusses a simple puzzle game which consists of 9 pieces, implemented with Windows Presentation Foundation. This article shows that drag and drop is not a difficult thing to implement in Windows Presentation Foundation.
Background
I am very grateful to Mr. Rudi Grobler who wrote the article "ListBox Drag & Drop Using Attached Properties". The code used in this application is based on the source code from his article. I am also very grateful to Mr. Seshi who write the article "8 Puzzle - WPF" whose application UI inspired this application's UI.
Using the Code
Classes in this application are:
Puzzle
Attributes and objects
puzzlePiece
: a collection which represents the puzzle pieces.name
Methods:
Puzzle
: constructor.OnEdit
: method to raise the event.Initialize
: method to load all the puzzle pieces and shuffle them.Validate
: validate the puzzle.
Events:
Edited
: event raised for each when a puzzle's arrangement is edited.
PuzzlePiece
Attributes and objects:
index
: the index of the piece.PuzzleImageSource
UriString
DragFrom
: to indicate whether the puzzle piece drags from ListBox
/Canvas
.
MainWindow
(UI)Attributes and objects:
puzzle
: represents the puzzle used in this application. The puzzle itself consists of a puzzlePiece
, which is of type ObservableCollection
and represents the puzzle pieces that must be arranged; name
, which is the puzzle name, and an event Edited
that is raised whenever the puzzle is edited.itemPlacement
: represents the placement of the puzzle pieces. It is used to validate the puzzle. It is a collection of puzzle pieces whose index represent the Canvas
' index, and it contains the puzzle piece dropped in the Canvas
itself.emptyItem
: represents an empty puzzle item that indicates if a Canvas
doesn't contain a puzzle item.lbDragSource
: A ListBox
object to refer to the ListBox
that raises the drag.cvDragSource
: A Canvas
object to refer to the Canvas
that raises the drag.
Method:
MainWindow()
: constructor.puzzleItemList_PreviewMouseLeftButtonDown
: handles a drag attempt to ListBox
.PzItmCvs_MouseLeftButtonDown
: handles a drag attempt to Canvas
.PuzzleItemDrop
: handles the drop to Canvas
.puzzle_Edited
: handles the edited event of the puzzle.GetDataFromCanvas
: gets data which will be transferred via drag drop if the drag is from a Canvas
.GetObjectDataFromPoint
: gets data which will be transferred via drag drop if the drag is from a ListBox
.instruction_Click
Application Scenario
The application's steps are:
- The puzzle pieces are loaded into the listbox.
- The player drags a puzzle piece from the
ListBox
on the left and drops it to one of the nine Canvas
es in the right. The player may also drag a puzzle piece from the Canvas
to the other Canvas
. If the destination Canvas
is empty, the puzzle piece will be moved. If it is not empty, the puzzle piece will be exchanged (see the screenshot). - For each puzzle edited, an event is raised and the puzzle will be validated. If the puzzle is correctly arranged, the player wins.
Screenshot
This application uses a very simple user interface, as shown in the figure below. In the left side, there is a ListBox
where the puzzle pieces are located. The puzzle piece will be dragged to one of the Canvas
es on the right side. A puzzle piece may be exchanged with another piece too by dragging and dropping it to a puzzle piece in another Canvas
.
Initialization
Firstly, the puzzle piece is loaded. This application can use more than one kind of puzzle. An argument passed to the Initialize
method is used to determine what puzzle is used. It can be generated randomly, but in this application, there is only one built-in puzzle, the rabbit puzzle. So the value 1 is passed as the method call argument. Originally, the initialization is sorted. So we must shuffle the puzzle after it has been initialized. This is done with the help of a random number built by .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));
}
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;
}
}
The collection object itemPlacement
in the MainWindow
class is used to map the Canvas
and the puzzle piece contained. The default value will be emptyItem
, which is an object with a certain value defined in the constructor. Then the ListBox
's item source is set with the puzzle piece and the edited handler defined.
Dragging
There are two elements that can be dragged in this application: ListBox
and the Canvas
. Both of them use the PreviewMouseLeftButtonDown
.
In the PreviewMouseLeftButtonDown
for the listbox, the data transferred through drag and drop operation is from the method GetObjectDataFromPoint
. This method uses the drag source and the mouse coordinate relative from the drag source as arguments. So, what's inside the method GetObjectDataFromPoint
? That's the method from Mr. Rudi Grobler from his article: ListBox Drag & Drop Using Attached Properties (see http://www.codeproject.com/KB/WPF/WPFDragDrop.aspx). We can guess how the code inside the method works by modifying it this way:
private object GetObjectDataFromPoint(ListBox dragSource, Point point)
{
UIElement element = dragSource.InputHitTest(point) as UIElement;
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;
}
if (element == dragSource)
{
return null;
}
}
if (data != DependencyProperty.UnsetValue)
{
return data;
}
}
return null;
}
Just uncomment the statement MessageBox.Show(...)
and run the application. From the image element which is dragged, iteratively seek its parent with the while
statement, until it reaches the ListBoxItem
. From the ListBoxItem
, we get the data from the method ItemFromContainer
. The data is an object of type PuzzlePiece
.
The PreviewMouseLeftButtonDown
for the Canvas
is identical, with a little modification to get the data from the Canvas
and doing a drag and drop operation. The difference is, in the Canvas
, we get the data directly from the itemPlacement
because in the Canvas
control, there is only the image, not the whole of the PuzzlePiece
object.
Dropping
There are four possible conditions in dropping:
- If the
Canvas
is empty and the puzzle piece is dragged from the ListBox
, the puzzle piece will be dropped. - If the
Canvas
is empty and the puzzle piece is dragged from the other Canvas
, the puzzle item will be moved there. - If the
Canvas
is not empty and the puzzle piece is dragged from the ListBox
, the puzzle item won't be dropped. - If the
Canvas
is not empty (there is another puzzle piece) and the puzzle piece is dragged from the other Canvas
, those two puzzle pieces will be exchanged.
When a puzzle piece is dropped, there are several things to do: drop it in the appropriate Canvas
, update the puzzle item placement, remove that item from the source (ListBox
/ Canvas
) if it is not switched, check if the puzzle is valid, and delete the value of the DragFrom
property. For conditions 1 and 2, the process is simple; first, we check if the Children
property of the Canvas
is 0 (it means it has no child element). Then, we define an image control whose width, height, and source is the image source's width and height, and the image source we got from the puzzle piece data is transferred from the drag operation. After that, the old puzzle piece is deleted and the placement is updated.
Image imageControl = new Image()
{
Width = destination.Width,
Height = destination.Height,
Source = itemTransferred.PuzzleImageSource,
Stretch = Stretch.UniformToFill
};
if (destination.Children.Count == 0)
{
destination.Children.Add(imageControl);
int indexToUpdate = int.Parse(destination.Tag.ToString());
if (itemTransferred.DragFrom == typeof(ListBox))
{
this.itemPlacement[indexToUpdate] = itemTransferred;
((IList)lbDragSource.ItemsSource).Remove(itemTransferred);
}
For condition 3, we only have to return from the method. For condition 4, first, we must get both of the indices, the source and the destination index. This is because we must exchange both of the puzzle pieces, the piece from the Canvas
which is dragged and the Canvas
where the puzzle piece is dropped. To change the Image
control which is in the Canvas
, the associated Canvas
is accessed with the method GetAssociatedCanvasByIndex
. This method takes an integer argument which is the index of the Canvas
to be accessed, and will return the associated Canvas
.
else if (destination.Children.Count > 0)
{
if (itemTransferred.DragFrom == typeof(ListBox))
{
return;
}
else if (itemTransferred.DragFrom == typeof(Canvas))
{
int sourceIndex = itemPlacement.IndexOf(itemTransferred);
int destinationIndex = int.Parse(destination.Tag.ToString());
Object buffer = null;
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;
buffer = itemPlacement[sourceIndex];
itemPlacement[sourceIndex] = itemPlacement[destinationIndex];
itemPlacement[destinationIndex] = buffer as PuzzlePiece;
buffer = null;
}
}
Conclusion
Corrections and suggestions are appreciated. I hope this article will help someone who is struggling with a drag and drop puzzle. I'm sorry for any mistakes I made in this article, and thank you very much.
Points of Interest
The difficult things in creating this application were: understanding the GetObjectDataFromPoint
method, and manually slicing the rabbit image with Microsoft Paint (believe me, that's difficult :D).
History
This is the first release of the puzzle application.