22

私が抱えている問題の簡単な例を次に示します。

<StackPanel Orientation="Horizontal">
    <Label>Foo</Label>
    <TextBox>Bar</TextBox>
    <ComboBox>
        <TextBlock>Baz</TextBlock>
        <TextBlock>Bat</TextBlock>
    </ComboBox>
    <TextBlock>Plugh</TextBlock>
    <TextBlock VerticalAlignment="Bottom">XYZZY</TextBlock>
</StackPanel>

TextBoxおよびを除くこれらの要素はすべて、ComboBox含まれるテキストの垂直方向の配置が異なり、見苦しく見えます。

Marginfor eachを指定することで、これらの要素のテキストを並べることができます。マージンがピクセル単位であり、ディスプレイの解像度やフォントサイズ、またはその他の変数とは関係がないことを除いて、これは機能します。

実行時にコントロールの正しい下マージンを計算する方法もわかりません。

これを行う最善の方法は何ですか?

4

7 に答える 7

19

問題

私が理解しているように、問題は、コントロールを水平にレイアウトしStackPanelて上に揃えたいが、各コントロールラインのテキストを揃えたいということです。Styleさらに、すべてのコントロールに何かを設定する必要はありませんMargin

基本的なアプローチ

問題の根本は、コントロールが異なれば、コントロールの境界とその中のテキストとの間の「オーバーヘッド」の量も異なることにあります。これらのコントロールを上に揃えると、その中のテキストが別の場所に表示されます。

したがって、各コントロールにカスタマイズされた垂直方向のオフセットを適用する必要があります。これは、すべてのフォント サイズとすべての DPI で機能するはずです。WPF は、デバイスに依存しない長さで機能します。

プロセスの自動化

Marginこれで、 を適用してオフセットを取得できますが、これは、 内のすべてのコントロールでこれを維持する必要があることを意味しStackPanelます。

これをどのように自動化しますか? 残念ながら、防弾ソリューションを得るのは非常に困難です。コントロールのテンプレートをオーバーライドすることができます。これにより、コントロールのレイアウト オーバーヘッドの量が変更されます。しかし、コントロール タイプ (TextBox、Label など) を特定のオフセットに関連付けることができる限り、多くの手動の配置作業を節約できるコントロールを作成することは可能です。

ソリューション

いくつかの異なるアプローチがありますが、これはレイアウトの問題であり、カスタムの Measure および Arrange ロジックが必要だと思います。

public class AlignStackPanel : StackPanel
{
    public bool AlignTop { get; set; }

    protected override Size MeasureOverride(Size constraint)
    {
        Size stackDesiredSize = new Size();

        UIElementCollection children = InternalChildren;
        Size layoutSlotSize = constraint;
        bool fHorizontal = (Orientation == Orientation.Horizontal);

        if (fHorizontal)
        {
            layoutSlotSize.Width = Double.PositiveInfinity;
        }
        else
        {
            layoutSlotSize.Height = Double.PositiveInfinity;
        }

        for (int i = 0, count = children.Count; i < count; ++i)
        {
            // Get next child.
            UIElement child = children[i];

            if (child == null) { continue; }

            // Accumulate child size.
            if (fHorizontal)
            {
                // Find the offset needed to line up the text and give the child a little less room.
                double offset = GetStackElementOffset(child);
                child.Measure(new Size(Double.PositiveInfinity, constraint.Height - offset));
                Size childDesiredSize = child.DesiredSize;

                stackDesiredSize.Width += childDesiredSize.Width;
                stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height + GetStackElementOffset(child));
            }
            else
            {
                child.Measure(layoutSlotSize);
                Size childDesiredSize = child.DesiredSize;

                stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
                stackDesiredSize.Height += childDesiredSize.Height;
            }
        }

        return stackDesiredSize; 
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        UIElementCollection children = this.Children;
        bool fHorizontal = (Orientation == Orientation.Horizontal);
        Rect rcChild = new Rect(arrangeSize);
        double previousChildSize = 0.0;

        for (int i = 0, count = children.Count; i < count; ++i)
        {
            UIElement child = children[i];

            if (child == null) { continue; }

            if (fHorizontal)
            {
                double offset = GetStackElementOffset(child);

                if (this.AlignTop)
                {
                    rcChild.Y = offset;
                }

                rcChild.X += previousChildSize;
                previousChildSize = child.DesiredSize.Width;
                rcChild.Width = previousChildSize;
                rcChild.Height = Math.Max(arrangeSize.Height - offset, child.DesiredSize.Height);
            }
            else
            {
                rcChild.Y += previousChildSize;
                previousChildSize = child.DesiredSize.Height;
                rcChild.Height = previousChildSize;
                rcChild.Width = Math.Max(arrangeSize.Width, child.DesiredSize.Width);
            }

            child.Arrange(rcChild);
        }

        return arrangeSize;
    }

    private static double GetStackElementOffset(UIElement stackElement)
    {
        if (stackElement is TextBlock)
        {
            return 5;
        }

        if (stackElement is Label)
        {
            return 0;
        }

        if (stackElement is TextBox)
        {
            return 2;
        }

        if (stackElement is ComboBox)
        {
            return 2;
        }

        return 0;
    }
}

StackPanel の Measure メソッドと Arrange メソッドから始めて、スクロール イベントと ETW イベントへの参照を削除し、存在する要素のタイプに基づいて必要な間隔バッファーを追加しました。このロジックは、水平スタック パネルにのみ影響します。

このAlignTopプロパティは、間隔によってテキストを上または下に揃えるかどうかを制御します。

コントロールがカスタム テンプレートを取得する場合、テキストの配置に必要な数値は変わる可能性がありますが、コレクション内の各要素に別のMarginまたはを配置する必要はありません。もう 1 つの利点は、配置に干渉することなく、子コントロールでStyle指定できるようになったことです。Margin

結果:

<local:AlignStackPanel Orientation="Horizontal" AlignTop="True" >
    <Label>Foo</Label>
    <TextBox>Bar</TextBox>
    <ComboBox SelectedIndex="0">
        <TextBlock>Baz</TextBlock>
        <TextBlock>Bat</TextBlock>
    </ComboBox>
    <TextBlock>Plugh</TextBlock>
</local:AlignStackPanel>

上の例を揃える

AlignTop="False":

下揃えの例

于 2011-04-29T02:34:46.490 に答える
7

マージンがピクセル単位であり、ディスプレイの解像度やフォントサイズ、またはその他の変数とは関係がないことを除いて、これは機能します。

あなたの仮定は正しくありません。(私は同じ仮定と同じ懸念を持っていたので知っています。)

実際にはピクセルではありません

まず、マージンはピクセル単位ではありません。(あなたはすでに私が頭がおかしいと思っていますよね?)FrameworkElement.Marginのドキュメントから:

厚さメジャーの既定の単位は、デバイスに依存しない単位 (1/96 インチ) です。

ドキュメントの以前のバージョンでは、これを「ピクセル」または後で「デバイスに依存しないピクセル」と呼ぶ傾向があったと思います。WPF は物理的なピクセルに関して実際には何もしないため、時間の経過とともに、彼らはこの用語が大きな間違いであることに気付くようになりました。彼らはこの用語を新しい何かを意味するために使用していましたが、聴衆はそれが何を意味するかを想定していました。それはいつも持っていました。そのため、ドキュメントは「ピクセル」への言及を避けることで混乱を避ける傾向があります。代わりに「デバイスに依存しないユニット」を使用するようになりました。

コンピューターのディスプレイ設定が 96dpi (デフォルトの Windows 設定) に設定されている場合、これらのデバイスに依存しない単位はピクセルと 1 対 1 で対応します。ただし、ディスプレイ設定を 120 dpi (以前のバージョンの Windows では「大きなフォント」と呼ばれていました) に設定した場合、Height="96" を含む WPF 要素は、実際には 120 物理ピクセルの高さになります。

したがって、マージンが「ディスプレイの解像度に関連していない」というあなたの仮定は正しくありません。WPF アプリを作成し、120dpi または 144dpi に切り替えてアプリを実行し、すべてが正常に動作することを確認することで、これを自分で確認できます。マージンが「ディスプレイの解像度とは関係ない」という懸念は、問題ではないことが判明しました。

(Windows Vista では、デスクトップを右クリックして [パーソナライズ] を選択し、サイドバーの [フォント サイズ (DPI) の調整] リンクをクリックして 120dpi に切り替えます。Windows 7 でも似たようなものだと思います。変更するとき。)

フォントサイズは問わない

フォントサイズに関しては、それも問題ではありません。これを証明する方法は次のとおりです。次の XAML をKaxamlまたはその他の WPF エディターに貼り付けます。

<StackPanel Orientation="Horizontal" VerticalAlignment="Top">  
  <ComboBox SelectedIndex="0">
    <TextBlock Background="Blue">Foo</TextBlock>
  </ComboBox>
  <ComboBox SelectedIndex="0" FontSize="100pt">
    <TextBlock Background="Blue">Foo</TextBlock>
  </ComboBox>
</StackPanel>

コンボボックスのフォント サイズは余白に影響しません

ComboBox クロムの太さがフォント サイズの影響を受けないことに注意してください。ComboBox の上部から TextBlock の上部までの距離は、デフォルトのフォント サイズを使用していても極端なフォント サイズを使用していても、まったく同じです。コンボボックスの組み込みマージンは一定です。

ラベルと ComboBox の両方のコンテンツに同じフォントを使用し、同じフォント サイズ、フォント スタイルなどを使用する限り、異なるフォントを使用しても問題ありません。ラベルの上部が整列し、トップが整列すれば、ベースラインも整列します。

そうです、マージンを使用してください

私は知っています、それはずさんに聞こえます。しかし、WPF には組み込みのベースライン アラインメントがなく、マージンは、この種の問題に対処するために提供されたメカニズムです。そして、マージンが機能するようにしました。

ここにヒントがあります。これを最初にテストしたとき、コンボボックスのクロムが 3 ピクセルの上部マージンに正確に対応するとは確信していませんでした。結局のところ、特にフォント サイズを含む WPF の多くの項目は、正確な非整数で測定されます。サイズを調整し、デバイスのピクセルにスナップします。丸めによって 120dpi または 144dpi の画面設定で位置がずれないようにするにはどうすればよいでしょうか?

答えは簡単です。コードのモックアップを Kaxaml に貼り付けてから、拡大します (ウィンドウの左下にズーム スライダー バーがあります)。ズームインしてもすべてが揃っていれば問題ありません。

次のコードを Kaxaml に貼り付けてからズームインを開始し、マージンが本当に適切であることを証明します。赤いオーバーレイが 100% ズーム、125% ズーム (120dpi) および 150% ズーム (144dpi) で青いラベルの上部と一列に並んでいる場合は、何にでも機能することを確信できます。私はそれを試してみました.ComboBoxの場合、クロムに整数サイズを使用したことがわかります. 上マージンを 3 にすると、ラベルが毎回 ComboBox テキストと整列します。

(Kaxaml を使用したくない場合は、XAML に一時的な ScaleTransform を追加して 1.25 または 1.5 にスケーリングし、まだ正しく動作することを確認してください。これは、優先する XAML エディターにない場合でも機能します。ズーム機能。)

<Grid>
  <StackPanel Orientation="Horizontal" VerticalAlignment="Top">  
    <TextBlock Background="Blue" VerticalAlignment="Top" Margin="0 3 0 0">Label:</TextBlock>
    <ComboBox SelectedIndex="0">
      <TextBlock Background="Blue">Combobox</TextBlock>
    </ComboBox>
  </StackPanel>
  <Rectangle Fill="#6F00" Height="3" VerticalAlignment="Top"/>
</Grid>
  • 100%:ラベル + コンボボックス + 余白、100%
  • 125%:ラベル + コンボボックス + 余白、125%
  • 150%:ラベル + コンボボックス + 余白、150%

彼らはいつも並んでいます。マージンは行く方法です。

于 2011-04-30T16:50:55.627 に答える
2

すべての UIElement には、ラベル、テキストブロック、およびその他のコントロールとは異なる内部パディングがいくつか関連付けられています。各コントロールのパディングを設定するとうまくいくと思います。**

Margin は、他の UIElement に対する相対的なスペースをピクセル単位で指定します。これは、サイズ変更やその他の操作で一貫していない可能性がありますが、パディングは各 UIElement の内部にあり、ウィンドウのサイズ変更には影響しません。

**

 <StackPanel Orientation="Horizontal">
            <Label Padding="10">Foo</Label>
            <TextBox Padding="10">Bar</TextBox>
            <ComboBox Padding="10">
                <TextBlock>Baz</TextBlock>
                <TextBlock>Bat</TextBlock>
            </ComboBox>
            <TextBlock Padding="10">Plugh</TextBlock>
            <TextBlock Padding="10" VerticalAlignment="Bottom">XYZZY</TextBlock>
        </StackPanel>

ここでは、すべてのコントロールにサイズ 10 の内部均一パディングを提供します。いつでも再生して、左、上、右、下のパディング サイズに関して変更できます。

パディングなし パディングあり

参照用に上記の添付のスクリーンショットを参照してください (1) パディングなしおよび (2) パディングあり これがお役に立てば幸いです...

于 2011-04-26T17:40:52.067 に答える
2

VerticalContentAlignment と Horizo​​ntalContentAlignment を指定し、各子コントロールのパディングとマージンを 0 に指定します。

于 2011-04-26T18:38:29.323 に答える
1

これを解決した方法は、固定サイズのマージンとパディングを使用することでした。

私が抱えていた本当の問題は、ユーザーがアプリケーション内でフォント サイズを変更できるようにしていたことでした。これは、Windows フォームの観点からこの問題に取り組んでいる人にとっては良いアイデアのように思えました。しかし、それはすべてのレイアウトを台無しにしました。12pt のテキストでは問題なく見えたマージンとパディングが、36pt のテキストではひどいものに見えました。

ただし、WPF の観点からは、私が実際に求めていたもの (ユーザーが自分の好みに合わせてサイズを調整できる UI) を達成するためのはるかに簡単な (そしてより良い) 方法はScaleTransform、ビューの上に配置してバインドすることでした。それとスライダーの値にScaleXScaleY

これは、ユーザーが UI のサイズをよりきめ細かく制御できるようにするだけでなく、UI のサイズに関係なく、物事を正しく並べるために行われたすべての配置と微調整が引き続き機能することを意味します。

于 2011-04-26T19:32:23.407 に答える
0

ComboBox と TextBlock の内部マージンが異なるため、これは注意が必要です。このような状況では、私は常にすべてを残して、VerticalAlignment を Center に設定しました。

代わりに、ComboBox から派生した独自の CustomControl を作成し、コンストラクターでそのマージンを初期化し、どこでも再利用します。

于 2009-12-31T08:00:29.617 に答える