tl;dr:カスタム WPF パネル クラスの定義済みWPF パネルの既存のレイアウト ロジックを再利用したいと考えています。この質問には、これを解決するための 4 つの異なる試みが含まれており、それぞれに異なる欠点があり、異なる障害点があります。また、小さなテスト ケースがさらに下にあります。
問題は、この目標を適切に達成するにはどうすればよいかです。
- カスタムパネルを定義しながら
- 別のパネルのレイアウト ロジックを内部的に再利用します。
- これを解決するための私の試みで説明されている問題に遭遇しましたか?
カスタム WPFパネルを作成しようとしています。このパネル クラスでは、推奨される開発手法に固執し、きれいな API と内部実装を維持したいと思います。具体的には、次のことを意味します。
- コードのコピーと貼り付けは避けたいです。コードのいくつかの部分が同じ機能を持っている場合、コードは一度だけ存在し、再利用する必要があります。
- 適切なカプセル化を適用し、外部ユーザーが安全に使用できるメンバーのみにアクセスできるようにしたいと考えています (内部ロジックを壊したり、内部の実装固有の情報を提供したりすることはありません)。
当面は、既存のレイアウトに固執するつもりです。別のパネルのレイアウト コードを再利用したいと思います (ここで提案されているように、レイアウト コードを再度記述するのではなく)。例として に基づいて説明DockPanel
しますが、どのような種類の にも基づいて、一般的にこれを行う方法を知りたいですPanel
。
レイアウト ロジックを再利用するために、パネルにビジュアルの子として を追加するつもりですDockPanel
。これにより、パネルの論理的な子が保持され、レイアウトされます。
これを解決する方法について 3 つの異なるアイデアを試しましたが、別のアイデアがコメントで提案されましたが、これまでのところ、それぞれが異なる時点で失敗しています。
1) カスタム パネルのコントロール テンプレートに内部レイアウト パネルを導入する
これは最も洗練されたソリューションのように思えます。このように、カスタム パネルのコントロール パネルは、ItemsControl
プロパティItemsPanel
がを使用しDockPanel
、プロパティがカスタム パネルのプロパティItemsSource
にバインドされているを特徴とすることができます。Children
残念ながら、Panel
は から継承してControl
いないため、Template
プロパティも、コントロール テンプレートの機能サポートもありません。
一方、Children
プロパティは によって導入されるPanel
ため、 には存在しません。また、意図した継承階層から抜け出して、実際にはであるが ではないControl
パネルを作成するのはハッキーと見なされる可能性があると思います。Control
Panel
2) 内側のパネルの子リストの単なるラッパーである、私のパネルの子リストを提供します
このようなクラスは次のようになります。UIElementCollection
パネル クラスをサブクラス化し、CreateUIElementCollection
メソッドのオーバーライド バージョンからそれを返しました。(ここでは実際に呼び出されるメソッドのみをコピーしました。他のメソッドは をスローするNotImplementedException
ように実装したので、他のオーバーライド可能なメンバーが呼び出されていないことは確かです。)
using System;
using System.Windows;
using System.Windows.Controls;
namespace WrappedPanelTest
{
public class TestPanel1 : Panel
{
private sealed class ChildCollection : UIElementCollection
{
public ChildCollection(TestPanel1 owner) : base(owner, owner)
{
if (owner == null) {
throw new ArgumentNullException("owner");
}
this.owner = owner;
}
private readonly TestPanel1 owner;
public override int Add(System.Windows.UIElement element)
{
return this.owner.innerPanel.Children.Add(element);
}
public override int Count {
get {
return owner.innerPanel.Children.Count;
}
}
public override System.Windows.UIElement this[int index] {
get {
return owner.innerPanel.Children[index];
}
set {
throw new NotImplementedException();
}
}
}
public TestPanel1()
{
this.AddVisualChild(innerPanel);
}
private readonly DockPanel innerPanel = new DockPanel();
protected override UIElementCollection CreateUIElementCollection(System.Windows.FrameworkElement logicalParent)
{
return new ChildCollection(this);
}
protected override int VisualChildrenCount {
get {
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) {
return innerPanel;
} else {
throw new ArgumentOutOfRangeException();
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
innerPanel.Measure(availableSize);
return innerPanel.DesiredSize;
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
innerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
}
これはほぼ正しく動作します。DockPanel
レイアウトは期待どおりに再利用されます。ElementName
唯一の問題は、バインディングがパネル内のコントロールを名前で (プロパティを使用して) 検索しないことです。
LogicalChildren
プロパティから内部の子を返そうとしましたが、これは何も変更しませんでした:
protected override System.Collections.IEnumerator LogicalChildren {
get {
return innerPanel.Children.GetEnumerator();
}
}
ユーザーArieによる回答では、クラスがこれに重要な役割を果たしていることが指摘されました。何らかの理由で、子コントロールの名前が関連するものに登録されません。これは、子ごとに呼び出すことで部分的に修正される可能性がありますが、正しいインスタンスを取得する必要があります。また、例えば子供の名前が変わったときの振る舞いが他のパネルと同じになるかどうかはわかりません。NameScope
NameScope
RegisterName
NameScope
代わりにNameScope
、インナーパネルの設定を行う方法のようです。TestPanel1
(コンストラクターで)簡単なバインディングでこれを試しました:
BindingOperations.SetBinding(innerPanel,
NameScope.NameScopeProperty,
new Binding("(NameScope.NameScope)") {
Source = this
});
残念ながら、これNameScope
は内部パネルの を に設定するだけnull
です。Snoopを使用して確認できる限り、実際のNameScope
インスタンスは、親ウィンドウのNameScope
添付プロパティ、またはコントロール テンプレート (またはおそらく他のキー ノードによって定義されたビジュアル ツリーのルート) にのみ格納されます。 )、タイプは問いません。もちろん、コントロール インスタンスは、その有効期間中にコントロール ツリーのさまざまな位置で追加および削除される可能性があるため、関連するNameScope
ものは時々変更される可能性があります。これも、バインディングが必要です。
残念ながら、 *the first found node that has a non- value assigned to the attached propertyRelativeSource
などの任意の条件に基づいてバインディングを定義することはできないため、ここで再び立ち往生します。null
NameScope
周囲のビジュアル ツリーの更新に対応する方法に関するこの他の質問が有用な回答をもたらさない限りNameScope
、特定のフレームワーク要素に現在関連するものを取得および/またはバインドするより良い方法はありますか?
3) 子リストが外側のパネルと同じインスタンスである内側のパネルを使用する
内側のパネルに子リストを保持し、呼び出しを外側のパネルの子リストに転送するのではなく、これは一種の逆の方法で機能します。ここでは、外側のパネルの子リストのみが使用されますが、内側のパネルは独自のリストを作成することはなく、単に同じインスタンスを使用します。
using System;
using System.Windows;
using System.Windows.Controls;
namespace WrappedPanelTest
{
public class TestPanel2 : Panel
{
private sealed class InnerPanel : DockPanel
{
public InnerPanel(TestPanel2 owner)
{
if (owner == null) {
throw new ArgumentNullException("owner");
}
this.owner = owner;
}
private readonly TestPanel2 owner;
protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
{
return owner.Children;
}
}
public TestPanel2()
{
this.innerPanel = new InnerPanel(this);
this.AddVisualChild(innerPanel);
}
private readonly InnerPanel innerPanel;
protected override int VisualChildrenCount {
get {
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) {
return innerPanel;
} else {
throw new ArgumentOutOfRangeException();
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
innerPanel.Measure(availableSize);
return innerPanel.DesiredSize;
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
innerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
}
ここでは、名前によるコントロールへのレイアウトとバインドが機能します。ただし、コントロールはクリックできません。
どうにかして呼び出しをHitTestCore(GeometryHitTestParameters)
内側HitTestCore(PointHitTestParameters)
のパネルに転送する必要があると思います。ただし、内側のパネルでは にしかアクセスできないため、元の実装が尊重していたであろう情報を失うことも無視することもなくInputHitTest
生のインスタンスを安全に処理する方法も、を処理する方法もわかりません。シンプル。HitTestResult
GeometryHitTestParameters
InputHitTest
Point
さらに、コントロールは、たとえば を押してフォーカスすることもできませんTab。これを修正する方法がわかりません。
さらに、内側のパネルと子の元のリストの間のどの内部リンクが、その子のリストをカスタムオブジェクトに置き換えることによって壊れているかわからないため、この方法には少し注意が必要です。
4) パネルクラスから直接継承
ユーザーClemensは、クラスに から直接継承させることを提案していDockPanel
ます。ただし、これが適切ではない理由が 2 つあります。
- 私のパネルの現在のバージョンは、 のレイアウト ロジックに依存し
DockPanel
ます。ただし、将来のある時点で、それでは十分ではなくなり、誰かが実際にパネルにカスタム レイアウト ロジックを記述しなければならなくなる可能性があります。その場合、インナーDockPanel
をカスタム レイアウト コードに置き換えるのは簡単ですがDockPanel
、パネルの継承階層から削除すると、重大な変更が発生します。 - 私のパネルが から継承している場合、パネルのユーザーは、特に
DockPanel
によって公開されているプロパティをいじって、レイアウト コードを妨害できる可能性があります。それだけのプロパティですが、すべてのサブタイプで機能するアプローチを使用したいと思います。たとえば、 から派生したカスタム パネルはおよびプロパティを公開します。これにより、自動生成されたレイアウトは、カスタム パネルのパブリック インターフェイスを介して完全に破棄される可能性があります。DockPanel
LastChildFill
Panel
Grid
ColumnDefinitions
RowDefinitions
説明されている問題を観察するためのテスト ケースとして、テスト対象のカスタム パネルのインスタンスを XAML で追加し、その要素内に以下を追加します。
<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Text, ElementName=tb1}" DockPanel.Dock="Left"/>
テキスト ブロックはテキスト ボックスの左側に配置し、現在テキスト ボックスに書かれている内容を表示する必要があります。
テキスト ボックスはクリック可能であり、出力ビューにはバインディング エラーが表示されないことが期待されます (したがって、バインディングも機能するはずです)。
したがって、私の質問は次のとおりです。
- 私の試みのいずれかを修正して、完全に正しい解決策に導くことはできますか? または、私が探していることをしようとしたことよりも好ましい完全に他の方法はありますか?