web-dev-qa-db-ja.com

水平StackPanelに配置された要素に、テキストコンテンツの共通ベースラインを共有させるにはどうすればよいですか?

これが私が抱えている問題のささいな例です:

<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>

TextBoxComboBoxを除くこれらの要素はすべて、含まれるテキストを垂直方向に異なる位置に配置し、見た目が醜いです。

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

実行時にコントロールの正しいボトムマージンをどのように計算するかさえわかりません。

これを行うための最良の方法は何ですか?

21
Robert Rossney

問題

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

基本的なアプローチ

問題の根本は、コントロールが異なれば、コントロールの境界とその中のテキストの間の「オーバーヘッド」の量も異なることです。これらのコントロールが上部に配置されている場合、内部のテキストはさまざまな場所に表示されます。

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

プロセスの自動化

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

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

ソリューション

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

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またはStyleを配置する必要はありません。もう1つの利点は、配置を妨げることなく、子コントロールで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>

align top example

AlignTop="False"

align bottom example

19
RandomEngy

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

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

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

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

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

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

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

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

(Windows Vistaでは、デスクトップを右クリックして[パーソナライズ]をクリックし、サイドバーの[フォントサイズ(DPI)を調整]リンクをクリックして120dpiに切り替えます。これはWindows7でも同様だと思います。毎回再起動する必要があることに注意してください。変更するとき。)

フォントサイズは関係ありません

フォントサイズに関しても、それは問題ではありません。これを証明する方法は次のとおりです。次の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 font size does not affect margins

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

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

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

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

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

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

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

(Kaxamlを使用したくない場合は、一時的なScaleTransformをXAMLに追加して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%の場合: Label + combobox + margin, 100 percent
  • 125%の場合: Label + combobox + margin, 125 percent
  • 150%の場合: Label + combobox + margin, 150 percent

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

7
Joe White

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

2
tcables

すべてのUIElementには、label、textblock、その他のコントロールとは異なる内部パディングがアタッチされています。各コントロールにパディングを設定することでうまくいくと思います。 ****

マージンは、サイズ変更やその他の操作で一貫性がない可能性がある他の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の内部均一パディングを提供しています。いつでも操作して、左、上、右、下のパディングサイズを変更できます。

Without PaddingWith Padding

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

2
Rohit Vats

これが役立つかもしれません:

<Window x:Class="Wpfcrm.MainWindow"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Wpfcrm"
        mc:Ignorable="d"
        Title="Business" Height="600" Width="1000" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="434*"/>
            <ColumnDefinition Width="51*"/>
            <ColumnDefinition Width="510*"/>
        </Grid.ColumnDefinitions>
        <StackPanel x:Name="mainPanel" Orientation="Vertical" Grid.ColumnSpan="3">
            <StackPanel.Background>
                <RadialGradientBrush>
                    <GradientStop Color="Black" Offset="0"/>
                    <GradientStop Color="White"/>
                    <GradientStop Color="White"/>
                </RadialGradientBrush>
            </StackPanel.Background>

            <DataGrid Name="grdUsers" ColumnWidth="*" Margin="0,-20,0,273" Height="272">

            </DataGrid>

        </StackPanel>

        <StackPanel Orientation="Horizontal" Grid.ColumnSpan="3">

            <TextBox Name="txtName" Text="Name" Width="203" Margin="70,262,0,277"/>
            <TextBox x:Name="txtPass" Text="Pass" Width="205" Margin="70,262,0,277"/>
            <TextBox x:Name="txtPosition" Text="Position" Width="205" Margin="70,262,0,277"/>
        </StackPanel>

        <StackPanel Orientation="Vertical" VerticalAlignment="Bottom" Height="217" Grid.ColumnSpan="3" Margin="263,0,297,0">
            <Button Name="btnUpdate" Content="Update" Height="46" FontSize="24" FontWeight="Bold" FontFamily="Comic Sans MS" Margin="82,0,140,0" BorderThickness="1">
                <Button.Background>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="Black"/>
                        <GradientStop Color="#FF19A0AE" Offset="0.551"/>
                    </LinearGradientBrush>
                </Button.Background>


            </Button>
        </StackPanel>

    </Grid>


</Window>
1
Kirill Shur

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

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

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

これにより、ユーザーはUIのサイズをよりきめ細かく制御できるだけでなく、UIのサイズに関係なく、正しく整列するために行われるすべての調整と調整が機能します。

1
Robert Rossney

ComboBoxとTextBlockの内部マージンが異なるため、これは注意が必要です。そのような状況では、私は常にすべてをCenterとしてVerticalAlignmentに任せましたが、見た目はあまり良くありませんが、かなり受け入れられます。

別の方法は、ComboBoxから派生した独自のCustomControlを作成し、コンストラクターでそのマージンを初期化して、どこでも再利用することです。

0
Akash Kava