Comment puis-je faire en sorte que les éléments disposés dans un empattement horizontal partagent une base de référence commune pour leur contenu textuel?
voici un exemple trivial du problème que j'ai:
<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>
chacun de ces éléments sauf le TextBox
et ComboBox
verticalement la position du texte qu'ils contiennent différemment, et il semble laid.
je peux aligner le texte de ces éléments vers le haut en spécifiant un Margin
pour chaque. Cela fonctionne, sauf que la marge est en pixels, et non par rapport à la résolution de l'écran ou la taille de la police ou autre chose qui va être variable.
Je ne sais même pas comment je calculerais la marge inférieure correcte pour un contrôle à l'exécution.
Quelle est la meilleure façon de le faire?
7 réponses
Le problème
si je comprends bien, le problème est que vous voulez disposer les commandes horizontalement dans un StackPanel
et aligner vers le haut, mais le texte de chaque ligne de contrôle. En outre, vous ne voulez pas avoir à mettre quelque chose pour chaque contrôle: soit un Style
et Margin
.
L'approche de base
la racine du problème est que les différentes commandes ont différentes quantités de "overhead" entre la limite de la commande et le texte à l'intérieur. Lorsque ces contrôles sont alignés en haut, le texte apparaît dans différents endroits.
alors ce que nous voulons faire, c'est appliquer un offset vertical personnalisé à chaque contrôle. Cela devrait fonctionner pour toutes les tailles de police et tous les DPIs: WPF fonctionne avec des mesures de longueur indépendantes de l'appareil.
automatiser le processus
Maintenant, nous pouvons appliquer un Margin
pour obtenir notre offset, mais cela signifie que nous devons le maintenir sur chaque contrôle dans le StackPanel
.
Comment automatiser cela? Malheureusement, il serait très difficile d'obtenir une solution à l'épreuve des balles; il est possible d'Outrepasser le gabarit d'une commande, ce qui changerait la quantité de disposition au-dessus de la commande. Mais il est possible de faire cuire un contrôle qui peut sauver beaucoup de travail d'alignement manuel, aussi longtemps que nous pouvons associer un type de contrôle (TextBox, Label, etc) avec un offset donné.
la solution
Il y a plusieurs différents approches que vous pouvez prendre, mais je pense que c'est un problème de mise en page et a besoin d'une Mesure personnalisée et Organiser logique:
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;
}
}
j'ai commencé à partir de la mesure du StackPanel et des méthodes D'arrangement, puis j'ai retiré les références au défilement et aux événements ETW et j'ai ajouté le tampon d'espacement nécessaire en fonction du type d'élément présent. La logique n'affecte que les panneaux de pile horizontaux.
AlignTop
contrôle de la propriété si l'espacement permet d'aligner le texte sur le haut ou bas.
Le nombre nécessaire d'aligner le texte peut changer si les contrôles d'un modèle personnalisé, mais vous n'avez pas besoin de mettre un autre Margin
ou Style
sur chaque élément de la collection. Un autre avantage est que vous pouvez maintenant spécifier Margin
sur les contrôles enfants sans interférer avec l'alignement.
Résultats:
<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"
:
cela fonctionne, sauf que la marge est en pixels, et non par rapport à la résolution de l'écran ou à la taille de la police ou à toute autre chose qui va être variable.
vos suppositions sont inexactes. (Je le sais, parce que j'avais les mêmes hypothèses et les mêmes préoccupations.)
Pas réellement pixels
tout d'Abord, la marge n'est pas en pixels. (Vous pensez déjà que je suis fou, non?) À partir de la documentation pour Cadrekelement.Marge:
l'unité par défaut pour une mesure D'épaisseur est l'unité indépendante de l'appareil (1 / 96e pouce).
je pense que les versions précédentes de la documentation avaient tendance à appeler cela un "pixel" ou, plus tard, un "pixel indépendant du périphérique". Avec le temps, ils ont réalisé que cette terminologie était une énorme erreur, parce que WPF ne fait pas vraiment en termes de pixels physiques -- ils utilisaient le terme pour signifier quelque chose de nouveau, mais leur public pensait que ça signifiait ce qu'il avait toujours. Donc maintenant les docs ont tendance à éviter la confusion en s'éloignant de toute référence aux "pixels"; ils utilisent maintenant "device-independent unit" à la place.
si les paramètres d'affichage de votre ordinateur sont définis à 96dpi (le paramètre par défaut de Windows), alors ces unités indépendantes de l'appareil correspondront un à un avec des pixels. Mais si vous avez paramétré vos paramètres d'affichage à 120 DPI (appelé "grandes polices" dans les versions précédentes de Windows), votre élément WPF avec Height= "96"aura en fait une hauteur de 120 pixels.
donc votre hypothèse que la marge "ne sera pas relative à la résolution de l'affichage" est incorrecte. Vous pouvez vérifier cela vous-même en écrivant votre application WPF, puis en passant à 120dpi ou 144dpi et en lançant votre application, et en observant que tout concorde toujours. Votre préoccupation que la marge est "pas par rapport à la résolution de l'écran" s'avère être un non-problème.
(dans Windows Vista, vous passez à 120dpi en cliquant avec le bouton droit de la souris sur le bureau > personnaliser, et en cliquant sur le lien "ajuster la taille de la police (DPI)" dans la barre latérale. Je crois que C'est quelque chose de similaire dans Windows 7. Attention que cela nécessite un redémarrage à chaque fois que vous changer.)
taille de la Police n'a pas d'importance
pour ce qui est de la taille de la police, c'est aussi un non-problème. Voici comment vous pouvez le prouver. Coller le XAML suivant dans Kaxaml ou tout autre FPF editeur:
<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>
observez que l'épaisseur du chrome ComboBox n'est pas affectée par la taille de la police. La distance entre le haut du ComboBox et le haut du texte est exactement la même, que vous utilisiez la taille de police par défaut ou une taille de police totalement extrême. la marge intégrée du combobox est constante.
cela n'a même pas d'importance si vous utilisez des polices différentes, tant que vous utilisez la même police pour les deux étiquette et le contenu de ComboBox, et la même taille de police, Style de police,etc. Les sommets des étiquettes de ligne, et si les sommets de la ligne, les lignes de base.
Donc oui, utiliser des marges
je sais, ça a l'air bâclé. Mais le FPF n'a pas d'alignement de base intégré, et les marges sont le mécanisme qu'ils nous ont donné pour traiter ce genre de problème. Et ils ont fait en sorte que les marges fonctionnent.
voici un conseil. Quand je l'ai testé pour la première fois, je n'étais pas convaincu que le le chrome de combobox correspondrait exactement à une marge supérieure de 3 pixels -- après tout, beaucoup de choses dans WPF, y compris et surtout les tailles de police, sont mesurées dans des tailles exactes, non intégrales et puis cassées aux pixels de l'appareil -- Comment pourrais-je savoir que les choses ne seraient pas mal alignées à 120 DPI ou 144dpi les paramètres de l'écran en raison de l'arrondissement?
la réponse s'avère facile: vous collez une maquette de votre code dans Kaxaml, puis vous zoomez (il y a une barre de zoom dans le coin inférieur gauche de la fenêtre). Si tout s'aligne même quand on est zoomé, alors tout va bien.
collez le code suivant dans Kaxaml, puis commencez à zoomer, pour vous prouver que les marges sont vraiment le chemin à suivre. Si le superposition rouge s'aligne avec le haut des étiquettes bleues à 100% de zoom, et aussi à 125% de zoom (120dpi) et 150% de zoom (144dpi), alors vous pouvez être assez sûr que cela fonctionnera avec n'importe quoi. Je l'ai essayé, et dans le cas de ComboBox, je peux vous dire qu'ils ont utilisé une taille intégrale pour chrome. Une marge supérieure de 3 permet à votre étiquette de s'aligner avec le texte de ComboBox à chaque fois.
(si vous ne voulez pas utiliser Kaxaml, vous pouvez juste ajouter un ScaleTransform temporaire à votre XAML pour le mettre à l'échelle à 1,25 ou 1,5, et s'assurer que les choses s'alignent toujours. Cela fonctionnera même si votre éditeur XAML préféré n'a pas de fonction de zoom.)
<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%:
- À 125%:
- à 150%:
ils font toujours la queue. Les marges sont la solution.
VerticalContentAlignment & HorizontalContentAlignment, puis spécifiez le rembourrage et la marge de 0 pour chaque contrôle enfant.
chaque élément comporte un rembourrage interne qui est différent pour l'étiquette, le texte et tout autre contrôle. Je pense que mettre du rembourrage pour chaque contrôle fera l'affaire. **
Marge spécifie l'espace par rapport à autres éléments en pixels qui peuvent ne pas être cohérent sur le redimensionnement ou tout autre opération alors que le rembourrage est interne pour chaque élément qui restent inchangés lors du redimensionnement de fenêtre.
**
<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>
Ici, je donne un interne uniforme rembourrage de taille 10 à chaque contrôle, vous pouvez toujours jouer avec elle pour changer de sujet à gauche,haut,droite,bas rembourrage tailles.
voir les captures d'écran ci-dessus pour référence (1) Sans capitonnage et (2) avec capitonnage J'espère que ce pourrait être d'une aide quelconque...
comment j'ai fini par résoudre ce problème était d'utiliser des marges et du rembourrage de taille fixe.
le vrai problème que j'avais était que je laissais les utilisateurs changer la taille de police dans l'application. Cela semblait être une bonne idée pour quelqu'un qui venait à ce problème du point de vue des formes de fenêtres. Mais il a foiré toute la mise en page; les marges et le rembourrage qui paraissait très bien avec 12pt texte paraissait terrible avec 36pt texte.
du point de vue du FPF, cependant, un beaucoup plus facile (et meilleur) moyen d'accomplir ce que j'essayais vraiment pour - un UI dont la taille l'utilisateur pourrait ajuster pour convenir à son goût - était de juste mettre un ScaleTransform
plus de la vue, et de se lier à son ScaleX
et ScaleY
à la valeur d'un curseur.
non seulement cela donne aux utilisateurs un contrôle beaucoup plus fin sur la taille de leur interface utilisateur, mais cela signifie aussi que tout l'alignement et les réglages effectués pour faire les choses alignées correctement fonctionne toujours indépendamment de la taille de l'interface utilisateur.
c'est délicat car ComboBox et Textlock ont des marges internes différentes. Dans de telles circonstances, j'ai toujours laissé tout pour avoir un alignement vertical comme centre qui n'a pas l'air très grand mais oui tout à fait acceptable.
Alternative est de créer votre propre CustomControl dérivé de ComboBox et d'initialiser sa marge dans le constructeur et de la réutiliser partout.
Peut-être cela va aider:
<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>