.Net windows forms custom layout engine
I am trying to write a card game, where a player is able to stack cards. E.g. Ace, Two, Three.
I would like to visualize a stack of cards, where the Ace card is partially covered by the Two card, and the Two card is partially covered by the Three card. The Three card is completely visible.
Easy, I thought. I make a user control where I add my cards: Controls.Add(ace); Controls.Add(two); etc.
Then I need something that is able to lay-out my Controls, so I wrote my custom LayoutEngine (derives from LayoutEngine). My first test does nothing more then shift the control 50 pixels.
After running the solution I noticed that the Z-ordering was wrong. Instead of the Three card being on top, the Ace card was on top looking like this:
Ace Card > Two card > Three card where: Ace Card is on top Two Card is under the Ace Card Three Card is under the Two Card.
So I started looking for a way to change the Z order in WinForms and found out that it is simply "not available". Like.. Huh?!
The alternative (provided by MS) is that the Z order can be altered, by setting the ChildIndex for the Controls. Jikes, that means that poking around in a list, change the behaviour of my application. Way to go MS...
Anyway, I tried all kind of things, but it seems impossible to write a layout engine that does the trick.
I've google-d all day, and found nothing useful. I am no GUI expert, so I run stuck on this lame issue. Who can help me out?
Your best bet is to avoid the use of controls entirely. They will A) result in poor(er) performance and B) complicate hit testing/drawing.
Simply create objects to represent the state of the table (I use a CardContainer object) and use Graphics.DrawImage to draw all of the cards where they lie during the paint event. You can use a single control for the entire table if you need to also add other UI elements.
This will also make animating card movement simpler should you decide to add animation.
I meant to expand this answer but was called away and simply posted what I had. Here are some details you may find useful. I created a "solitaire game engine". The engine hosts one solitaire game at a time (klondike, spider, strategy, etc.). It tracks statistics for each game and allows both playing and editing of the individual games. The games are IronPython scripts which makes adding new games relatively easy.
My CardContainer is an object that holds zero or more cards.
It has a LieDirection (None, Up, Down, Left, Right) which determains how its cards are laid out.
It has a MaximumDepth that clamps the number of cards drawn in the LieDirection. This is handy for games like Klondike where you only want to show the top 3 cards of the waste.
It has properties for spacing the cards. There are separate spacing values for cards that are face up and face down. It can auto-pack cards into an area defined by MaximumLength. And it has an 'extra pad' value, one for each card--whether there is a card at that index or not. The latter is used during a simulated mouse hover to 'uncover' the card pointed to so that the user can clearly see a card that might be obscured by cards on top of it. This is accomplished by setting the 'extra pad' of the card on top of the hover card. This could have been simplified by having a "hover card" and "hover spacing" property, but having extra padding per-card allows for odd kinds of solitaire games that highlight a particular 'row' in the tableau piles with spacing.
It has a HitTest method to return a Card from a given X,Y location.
All of that means that the Card object has no notion of where it is drawn on the table. I have a complex animation system and so a card's location ultimately comes from the animation engine. If a card is not currently animating, the animation system gets its location from its container.
Note that the card's location referred to above is strictly for drawing. All cards are always attached to exactly one CardContainer and are simply moved from one to another. There is one 'special' container called the Deck which initially contains every card. It is positioned off of the table initially. A container has a Visible property. Animations play only if moving a card from a Visible container to another Visible container. This allows you to move cards around without animation when necessary and cards can "fly out" to/from containers positioned off the table.
The engine also has a rudamentary layout system for positioning CardContainers relative to each other. One very handy thing I did was to use a card-size-relative coordinate system. The 'width' of the table is exactly 11 card widths. No matter how big the user sizes the table, the width is always 11 card widths. This means that the card sizes (as viewed by the user) grow and shrink. The height is variable, but is determained by a fixed card-sized ratio (determained from the card bitmaps). If you give a CardContainer an X value of 1.0, that means it will be located one card-width from the left of the table. The values are floating points so you can specify 1/2 a card-width with 0.5. This makes it very easy to position elements in the script without having to worry about screen coordinates. No matter how the user alters the size of the screen, your game will be laid out exactly the same way.
The engine also has unlimited undo and redo. This means that not only do card moves (from one container to another) have to be recorded, but all property changes are recorded as well (both card and container properties). Undo and redo can be difficult to implement if not planned for from the start. The scripts have access to a Game.LogVariableChange method so that they can alter the value of a global variable through the recording mechanism. This is necessary for something like Klondike's "three redeals" feature. The script has to store the number of redeals used, but if the user undid a redeal, that variable's value change has to be undone too.
This works very well for Solitaire, but could work for just about any kind of card game. Obviously you don't have to go and implement all of this your first time out. I present the information just to give you some ideas.