最善の策は、コントロールの使用を完全に避けることです。A) パフォーマンスが低下し、B) ヒット テスト/描画が複雑になります。
テーブルの状態を表すオブジェクトを作成し (私は CardContainer オブジェクトを使用します)、Graphics.DrawImage を使用して、ペイント イベント中に配置されているすべてのカードを描画します。他の UI 要素も追加する必要がある場合は、テーブル全体に 1 つのコントロールを使用できます。
これにより、アニメーションを追加することにした場合に、カードの動きのアニメーション化も簡単になります。
更新しました
私はこの回答を拡張するつもりでしたが、呼び出されて、単に私が持っていたものを投稿しました. ここでは、役に立つと思われる詳細をいくつか示します。「ソリティアゲームエンジン」を作りました。エンジンは、一度に 1 つのソリティア ゲーム (クロンダイク、スパイダー、戦略など) をホストします。各ゲームの統計を追跡し、個々のゲームのプレイと編集の両方を可能にします。ゲームは、新しいゲームの追加を比較的簡単にする IronPython スクリプトです。
My CardContainer は、0 個以上のカードを保持するオブジェクトです。
カードの配置方法を決定する LieDirection (None、Up、Down、Left、Right) があります。
LieDirection で描かれたカードの数をクランプする MaximumDepth があります。これはクロンダイクのような無駄カードの上位 3 枚だけを表示したいゲームに便利です。
カードの間隔をあけるプロパティがあります。表向きと裏向きのカードには、別々の間隔の値があります。MaximumLength で定義された領域にカードを自動パックできます。また、そのインデックスにカードがあるかどうかに関係なく、カードごとに 1 つの「追加パッド」値があります。後者は、シミュレートされたマウスのホバー中に使用され、指しているカードを「明らかに」して、ユーザーがカードの上にあるカードによって隠されている可能性のあるカードをはっきりと見ることができるようにします。これは、カードの「追加パッド」をホバー カードの上に設定することによって実現されます。これは、「ホバー カード」と「ホバー間隔」プロパティを使用することで単純化できますが、カードごとに追加のパディングを使用すると、タブロー パイルの特定の「行」を間隔で強調表示する奇妙な種類のソリティア ゲームが可能になります。
特定の X、Y 位置から Card を返す HitTest メソッドがあります。
つまり、Card オブジェクトには、テーブル上のどこに描画されるかという概念がありません。複雑なアニメーション システムを使用しているため、カードの位置は最終的にアニメーション エンジンから取得されます。カードが現在アニメーション化されていない場合、アニメーション システムはコンテナからその位置を取得します。
上記のカードの場所は、厳密にドロー用であることに注意してください。すべてのカードは常に 1 つの CardContainer に接続され、単純に別のカードに移動されます。デッキと呼ばれる 1 つの「特別な」コンテナがあり、最初はすべてのカードが含まれています。最初はテーブルから離れた場所に配置されています。コンテナには Visible プロパティがあります。アニメーションは、Visible コンテナから別の Visible コンテナにカードを移動する場合にのみ再生されます。これにより、必要に応じてアニメーションなしでカードを移動することができ、カードはテーブルの外にあるコンテナに出入りすることができます。
このエンジンには、CardContainers を相互に相対的に配置するための基本的なレイアウト システムもあります。私が行った非常に便利な方法の 1 つは、カード サイズに相対的な座標系を使用することでした。テーブルの「幅」はちょうど 11 カードの幅です。ユーザーがテーブルのサイズをどれだけ大きくしても、幅は常に 11 カード幅です。これは、(ユーザーから見た) カードのサイズが増減することを意味します。高さは可変ですが、固定のカード サイズの比率 (カード ビットマップから決定) によって決定されます。CardContainer に 1.0 の X 値を指定すると、テーブルの左から 1 カード幅に配置されることを意味します。値は浮動小数点であるため、0.5 でカード幅の 1/2 を指定できます。これにより、画面座標を気にすることなく、スクリプト内の要素を非常に簡単に配置できます。ユーザーが画面のサイズをどのように変更しても、
エンジンには、無制限の取り消しとやり直しもあります。これは、(あるコンテナから別のコンテナへの) カードの移動を記録する必要があるだけでなく、すべてのプロパティの変更も記録する必要があることを意味します (カードとコンテナのプロパティの両方)。元に戻すとやり直しは、最初から計画していない場合、実装が難しい場合があります。スクリプトは Game.LogVariableChange メソッドにアクセスできるため、記録メカニズムを通じてグローバル変数の値を変更できます。これは、クロンダイクの「3 回のリディール」機能などに必要です。スクリプトは使用された再取引の回数を保存する必要がありますが、ユーザーが再取引を元に戻した場合、その変数の値の変更も元に戻す必要があります。
これは、ソリティアでは非常にうまく機能しますが、ほぼすべての種類のカード ゲームで機能します。もちろん、最初からすべてを実装する必要はありません。アイデアを提供するためだけに情報を提示します。