How to Vertically “center” align the multi line te

2019-07-24 19:48发布

Sorry if the title a bit misleading, I have difficulty to entitle it. It's also not exactly "center". (see below for further explanations)

Hi, I am going to make a project to write numbered musical scores. But I found an obstacle on leveling the "font". (See below picture for visual)

enter image description here

  1. I need 1 2 3 3 1 2 3 4 4 to be in a straight "line"
  2. I use WrapPanel as the container of my notes (as in the screen).

The main problem is on the pitch dot, which located EITHER above or below the note. Pitch is bound to the note, so I need process it in 1 User Control. But, if so, then I can't control the placement of the note "font" to be in one line.

Below is my note user control code :

XAML

<UserControl x:Class="RevisiConverter.NumericNoteBox"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" Width="{Binding ActualWidth, ElementName=txbNote}" 
         Height="{Binding ActualHeight, ElementName=mainStack}">
<StackPanel Name="mainStack">
    <StackPanel Name="topStack">

    </StackPanel>
    <Canvas Name="canvas" Width="{Binding ActualWidth, ElementName=txbNote}" Height="{Binding ActualHeight, ElementName=txbNote}"
            HorizontalAlignment="Center">            
        <TextBlock Name="txbNote" Text="1" Foreground="Black" FontSize="15"
                   Margin="0,0,0,-3" FontFamily="Courier" Canvas.Top="0" Canvas.Left="0"/>
    </Canvas>
    <StackPanel Name="botStack">

    </StackPanel>
</StackPanel>

XAML.CS

public partial class NumericNoteBox : UserControl
{
    private Note _child;
    private Line _sharp;

    public Note Child
    {
        get { return _child; }
        set
        {
            _child = value;
            Update();
        }
    }

    public NumericNoteBox()
    {
        InitializeComponent();

        _sharp = null;
    }

    public void Update()
    {
        txbNote.Text = _child.MainNote.ToString();

        if (_sharp != null)
        {
            _sharp.Visibility = Visibility.Hidden;
        }

        topStack.Children.Clear();
        botStack.Children.Clear();

        if (_child != null)
        {
            if (_child.Pitch > 0)
            {
                for (int i = 0; i < _child.Pitch; i++)
                {
                    topStack.Children.Add(new Ellipse());
                    (topStack.Children[topStack.Children.Count - 1] as Ellipse).Width = 3;
                    (topStack.Children[topStack.Children.Count - 1] as Ellipse).Height = 3;
                    (topStack.Children[topStack.Children.Count - 1] as Ellipse).Fill = Brushes.Black;

                    if (_child.Accidental != Note.Accidentals.Flat)
                    {
                        (topStack.Children[topStack.Children.Count - 1] as Ellipse).Margin = new Thickness(0, 1, 0, 0);
                    }
                    else
                    {
                        (topStack.Children[topStack.Children.Count - 1] as Ellipse).Margin = new Thickness(8, 1, 0, 0);
                    }
                }
            }
            else if (_child.Pitch < 0)
            {
                for (int i = 0; i < Math.Abs(_child.Pitch); i++)
                {
                    botStack.Children.Add(new Ellipse());
                    (botStack.Children[botStack.Children.Count - 1] as Ellipse).Width = 3;
                    (botStack.Children[botStack.Children.Count - 1] as Ellipse).Height = 3;
                    (botStack.Children[botStack.Children.Count - 1] as Ellipse).Fill = Brushes.Black;

                    if (_child.Accidental != Note.Accidentals.Flat)
                    {
                        (botStack.Children[botStack.Children.Count - 1] as Ellipse).Margin = new Thickness(0, 1, 0, 0);
                    }
                    else
                    {
                        (botStack.Children[botStack.Children.Count - 1] as Ellipse).Margin = new Thickness(8, 1, 0, 0);
                    }
                }
            }

            if (_child.Accidental == Note.Accidentals.Flat)
            {
                txbNote.Text = "b" + _child.MainNote.ToString();
            }
            else if (_child.Accidental == Note.Accidentals.Sharp)
            {
                if (_sharp == null)
                {
                    _sharp = new Line();
                    _sharp.X1 = 10;
                    _sharp.Y1 = 2.5;
                    _sharp.X2 = -2.5;
                    _sharp.Y2 = 12.5;
                    _sharp.StrokeThickness = 1;
                    _sharp.Stroke = Brushes.Black;

                    canvas.Children.Add(_sharp);
                }

                _sharp.Visibility = Visibility.Visible;
            }
        }
    }
}

Note :

  1. I'm not bound to that code, so if any of you have dealt to things like this more efficiently with totally different approach, then it's always welcome.
  2. Sorry for some grammar error, if any, since english is not my first language.
  3. I feel that I have not explained my problem real clear, since I also quite confused on it, so, please clarify anything you need.

Thanks

ADDITIONAL DETAILS :

  1. The dot theoretically doesn't have certain limitation, but in practice, it's usually only 3 maximum (either 3 on top or 3 on bottom - not both)
  2. In my code above, the note user control is divided into 3 grid row, the top one is for stacking the top dot, the mid one is for visualizing the note (numbers) and the bot one is for stacking the bot dot.
  3. Please clarify anything, and I'll add more.

1条回答
2楼-- · 2019-07-24 20:09

You should do this with an ItemsControl which use an appropriate ItemTemplate for the numeric notes.

First, create a ViewModel that defines a collection of "numeric note" items:

public class NumericNoteItem
{
    public int Number { get; set; }
    public int Pitch { get; set; }
}

public class ViewModel
{
    public ViewModel()
    {
        NumericNotes = new ObservableCollection<NumericNoteItem>();
    }

    public ObservableCollection<NumericNoteItem> NumericNotes { get; private set; }
}

In the constructor of your MainWindow, you may set the DataContext to an instance of this ViewModel like this:

public MainWindow()
{
    InitializeComponent();

    var vm = new ViewModel();
    vm.NumericNotes.Add(new NumericNoteItem { Number = 1, Pitch = 0 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 2, Pitch = 1 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 3, Pitch = -1 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 4, Pitch = 0 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 5, Pitch = 2 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 6, Pitch = -2 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 4, Pitch = 0 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 5, Pitch = 3 });
    vm.NumericNotes.Add(new NumericNoteItem { Number = 6, Pitch = -3 });

    DataContext = vm;
}

For visualizing the pitch I'd recommend to use a Path object with a Geometry that consists of a number of EllipseGeometries. To achieve this you would implement a binding converter that converts the pitch number into a Geometry, like shown below. It uses the parameter argument of the Convert method to create a Geometry for either positive or negative pitch values.

public class NotePitchConverter : IValueConverter
{
    private const double radius = 1.5;
    private const double distance = 2 * radius + 1;

    public object Convert(
        object value, Type targetType, object parameter, CultureInfo culture)
    {
        var pitch = (int)value;
        var geometry = new GeometryGroup();

        if (parameter as string == "Bottom")
        {
            pitch = -pitch;
        }

        for (int i = 0; i < pitch; i++)
        {
            geometry.Children.Add(new EllipseGeometry(
                new Point(radius, radius + i * distance), radius, radius));
        }

        return geometry;
    }

    public object ConvertBack(
        object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

Now, in XAML you would create an instance of this converter in e.g. the MainWindow Resources:

<Window.Resources>
    <local:NotePitchConverter x:Key="NotePitchConverter"/>
</Window.Resources>

and write the ItemsControl like shown below. Note that the DataTemplate uses a fixed height for the Path elements that show the top and bottom pitch. If you need to show more than three dots, you'd need to increase their height.

<ItemsControl ItemsSource="{Binding NumericNotes}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid Margin="2">
                <Grid.RowDefinitions>
                    <RowDefinition Height="12"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="12"/>
                </Grid.RowDefinitions>
                <Path Grid.Row="0" Fill="Black" HorizontalAlignment="Center"
                      VerticalAlignment="Bottom"
                      Data="{Binding Pitch,
                             Converter={StaticResource NotePitchConverter}}"/>
                <TextBlock Grid.Row="1" Text="{Binding Number}"/>
                <Path Grid.Row="2" Fill="Black" HorizontalAlignment="Center"
                      VerticalAlignment="Top"
                      Data="{Binding Pitch,
                             Converter={StaticResource NotePitchConverter},
                             ConverterParameter=Bottom}"/>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
查看更多
登录 后发表回答