I developed some prototype of a custom layout panel on Universal Windows Platform called DynamicSplitter that contains multiple child panes and arranges them horizontally or vertically depending on its IsHorizontal property of type bool. To support heterogeneous layout of panes, DynamicSplitter itself is used as the child pane of another DynamicSplitter with different IsHorizontal property. So, for example, the parent splitter can have two panes split horizontally one of which is a user control and other is the child splitter, and the child splitter can have three panes split vertically and so on.
Regardless of DynamicSplitter has a lot in common with Grid or StackPanel it is inherited directly from Panel and lays out its child panes by overriding MeasureOverride(…) and ArrangeOverride(…) functions:
public ref class DynamicSplitter sealed : public Windows::UI::Xaml::Controls::Panel { public: DynamicSplitter(); typedef Windows::UI::Xaml::FrameworkElement Pane; ... protected: Size MeasureOverride(Size availableSize) override; Size ArrangeOverride(Size finalSize) override; //factory methods for creating child panes and child splitters virtual Pane ^ CreatePane(); virtual DynamicSplitter ^ CreateSplitter(); ... private: static const int TRACK_BAR_THICKNESS = 8; bool bHorizontal; typedef std::vector<float> FloatVector; //array containing relative sizes of the panes (ratios), //sum of all the ratios is 1.0, used to resize the panes proportionally. FloatVector Ratios; property bool IsHorizontal { bool get() { return bHorizontal; } } int GetPaneCount() const { return Ratios.size(); } void ArrangePanes(Size finalSize) { MeasureOrArrangePanes(finalSize, false); } void MeasurePanes(Size finalSize) { MeasureOrArrangePanes(finalSize, true); } void MeasureOrArrangePanes(Size size, bool measure); ... }
MeasureOverride(…) and ArrangeOverride(…) functions call MeasureOrArrangePanes(…) internally that calls Measure(…) or Arrange(…) on child panes depending on measure parameter:
Size DynamicSplitter::MeasureOverride(Size availableSize) { MeasurePanes(availableSize); return availableSize; //did not work before } Size DynamicSplitter::ArrangeOverride(Size finalSize) { ArrangePanes(finalSize); return __super::ArrangeOverride(finalSize); } void DynamicSplitter::MeasureOrArrangePanes(Size size, bool measure) { //float unity = std::accumulate(Ratios.begin(), Ratios.end(), 0.0F); //total length of all the panes with the track bars float total_length = (IsHorizontal ? size.Height : size.Width); const float left_or_top_org = 0; float sum_ratio = 0; for (int i = 0; i < (int)Ratios.size(); ++i) { float ratio = Ratios[i]; float left_or_top = left_or_top_org + sum_ratio * total_length; float beg = left_or_top; if (i != 0) { beg += TRACK_BAR_THICKNESS / 2; } float len = total_length * ratio - TRACK_BAR_THICKNESS; if (i == 0) { len += TRACK_BAR_THICKNESS / 2; } //0 could be the last pane if there is only one pane if (i == (int)Ratios.size() - 1) { //the last len accumulates all the inacurracy len = total_length - beg; } Rect rcPane; if (IsHorizontal) { rcPane = Rect(Point(0, beg), Point(size.Width, beg + len)); } else { rcPane = Rect(Point(beg, 0), Point(beg + len, size.Height)); } //it can be either Splitter or Pane Pane ^ pane = GetPaneByIndex(i); if (measure) { pane->Measure(Size(rcPane.Width, rcPane.Height)); } else { pane->Arrange(rcPane); } sum_ratio += ratio; } }
This code works nicely, at the picture below you can see three panes with the red border:
All the panes are of the same type derived from UserControl and contain ListView with cyan border, ItemsControl with yellow border and buttons for adding/deleting items. Buttons at the top-right corner of the pane are used to split the pane horizontally (H) or vertically (V) and to close the pane (X). If I add 1000 (1K) items to ListView and ItemsControl I can can scroll the lists with vertical scroll bar:
The rest of the article describes a bug that Microsoft fixed with some Windows 10 update, so ItemsControl does not glitch with DynamicSplitter anymore and you can stop reading here.
So we have some beautiful pictures and working controls, but the the only issue is that I face some terrible bug with scrolling of ItemsControl: If I close second pane and so cause the execution of code that does some manipulation with the visual tree (see below) the vertical scrolling of ItemsControl stops working, and an empty space appears instead of ItemsControl rows:
But resizing the main window a bit or adding an item to the collection bound to ItemsControl.ItemsSource make the scrolling work again, the empty space is filled with the rows and all items are scrolled fine:
Below I provided the code that brakes my ItemsControl scrolling:
//pPane is the pane to be closed DynamicSplitter ^ pParentSplitter = dynamic_cast<DynamicSplitter ^>(VisualTreeHelper::GetParent(this)); int nPaneIndex = GetPaneIndex(pPane); int second_pane_index = nPaneIndex == 0 ? 1 : 0; //pSecondPane contains our ItemsControl //and we change its parent by removing from //child splitter and adding to the parent splitter Pane ^ pSecondPane = GetPaneByIndex(second_pane_index); int my_index = pParentSplitter->GetPaneIndex(this); Children->RemoveAt(second_pane_index); pParentSplitter->Children->RemoveAt(my_index); pParentSplitter->Children->InsertAt(my_index, pSecondPane); pParentSplitter->InvalidateArrange();
Can anyone tell me what I did wrong? Or probably ItemsControl does not like to be moved across the parent panels? Below I provided the source code of my ItemsControl:
<!-- We Add Borders around each ListView Item, to make it look like a grid, you can change this. --> <Style x:Key="ItemBorder" TargetType="Border"> <Setter Property="BorderBrush" Value="Gray" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="Background" Value="White" /> </Style> <Style x:Key="ColumnItemBorder" TargetType="Border"> <Setter Property="BorderBrush" Value="Gray" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="Background" Value="Silver" /> </Style> <Style TargetType="local:DataGrid" > <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:DataGrid"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ScrollViewer Grid.Row="1" HorizontalScrollMode="Auto" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"> <ScrollViewer.TopHeader> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="60" /> <ColumnDefinition Width="100" /> <ColumnDefinition Width="90" /> <ColumnDefinition Width="90" /> </Grid.ColumnDefinitions> <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="0"> <TextBlock>Id</TextBlock> </Border> <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="1"> <TextBlock>Name</TextBlock> </Border> <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="2"> <TextBlock>LAT</TextBlock> </Border> <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="3"> <TextBlock>LON</TextBlock> </Border> </Grid> </ScrollViewer.TopHeader> <!-- Our ListView's Regular Rows. --> <ItemsPresenter HorizontalAlignment="Left" VerticalAlignment="Top" /> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <!-- VirtualizingStackPanel produces this: WinRT information: The TopLeftHeader, TopHeader and LeftHeader properties cannot be used when the Content is an OrientedVirtualizingPanel, VirtualizingStackPanel, CarouselPanel or WrapGrid instance. <VirtualizingStackPanel Orientation="Vertical"></VirtualizingStackPanel> so we use StackPanel --> <ItemsStackPanel Orientation="Vertical"></ItemsStackPanel> <!--<StackPanel Orientation="Vertical"></StackPanel>--> </ItemsPanelTemplate> </Setter.Value> </Setter> </Style>
by the way, changing ItemsStackPanel with StackPanel solves the issue, but of cause the performance of StackPanel is not acceptable.