问题
I have a Template
that is a List<List<Location>>
. Each List<Location>
is a row and each Location
is a column. Each row is centered, with the Locations
at a fixed width (i.e., the locations themselves don't stretch). Since the rows have different numbers of columns (not to mention the list having a variable number of rows), This is not a simple actual "grid".
The empty spots are where some cells have been "turned off". You'll also notice that cells can be portrait or landscape.
Currently the style of the cells has them at a fixed size; I'm also including the rows/columns template here:
<Style TargetType="ContentControl" x:Key="imageLocationStyle">
<!--Default styling (Portrait, Active)-->
<Setter Property="Height" Value="{StaticResource fullLong}"/>
<Setter Property="Width" Value="{StaticResource fullShort}"/>
<Setter Property="BorderBrush" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Landscape}" Value="True">
<Setter Property="Height" Value="{StaticResource fullShort}"/>
<Setter Property="Width" Value="{StaticResource fullLong}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Active}" Value="False">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type data:ImageTemplate}">
<!--Rows-->
<ItemsControl ItemsSource="{Binding ImageLocations}" VerticalAlignment="Center">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--Columns-->
<ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!--Single cell-->
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--Each presenter of a Template will provide its own
DataTemplate for TemplateImageLocation, which will
determine how each cell is rendered.-->
<ContentControl Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
I want the cells (and thus the template) to be of variable, flexible size, relative to the size of the Template
's container element. The example above is cut from a UniformGrid
that contains Button
s, which as you can see by the selection, fill their container. I've set the xaml properties fullLong
and fullShort
to a set number which is untenable, not only because any other time I want to use a template in another context I'd have to define a new style with another relatively arbitrary fixed number, but because screen sizes are different not to mention template sizes are different.
I've tried using a UniformGrid
for the template (1 column) and each row (1 row), but then all rows are the same width, which is not what I want.
I've thought about padding all the rows with the max number of cells (happens to be 7), but as far as I can imagine, that wouldn't account for the difference between portrait and landscape. A row with 7 portrait cells should take up less horizontal space than a row with 7 landscape cells.
I believe if I simply had a way to determine the long
and short
dimensions as a percentage of their container, everything else would work fine. If I wanted a template to stretch to fill all or most available space depending on the size of the template itself (number of cells), I'd need to figure out the length of the longest row and/or column to determine what the percentage should even be.
How can this be done? Do I need to do all the calculations in a viewmodel and bind styles to that? How would I put all of it together?
回答1:
This is actually an interesting problem. I think using nested ItemsControl
s was the right approach- it was my first thought before I saw your code- but I think your requirements are a bit too complicated for that. You're going to need to go custom; a custom control.
It's a bit of work, but I think it's the "right" way to do things, plus it's good experience in my opinion.
Base Type
I thought maybe a custom panel would be the right fit. Panel is the base class for "containers" which "position and arrange child objects". And that is exactly what you need, a custom arrangement of child objects. Implementing a custom Panel
also gives you access everything you need, specifically the MeasureOverride method, which gives you availableSize
, telling you how much space you have to work with. It also lets you dictate a size for each child element, which you can calculate based off availableSize
and the number of children.
The problem is Panel
s generally support adding any kind of visual content, and the content you want to display is restricted to a specific data structure. I also thought of a custom ItemsControl
, but we get the same problem. ItemsControl
s are designed to be independent of the ItemsPanel
that they use.
For this reason, I think you should just inherit from FrameworkElement
. It's one level down from Panel
and still includes MeasureOverride
.
Data Source Property
For your data, you need to end up with an IList<IList<Location>>
. I recommend using IList<T>
over List<T>
so as not to restrict the input type unnecessarily. This will allow for List<T>
, but also other collections like ObservableCollection<T>
.
Since you will only be expecting input of that type, there's no real need to implement a flexible input property like ItemsControl.ItemsSource
. You should, however, do the following:
Include a PropertyChangedCallback so you will be notified when the data source changes. This way you know to invalidate the current layout of the control.
In the
PropertyChangedCallback
, check if theIList<T>
provided also implementsINotifyCollectionChanged
(such asObservableCollection<T>
). If so, attach a handler to the CollectionChanged for the same reason as above. Make sure to remove the existing handler if the data source property is given a new value.
Item Elements
Each Location
needs a visual item to represent it. I would recommend sticking with ContentControl
(this is what the base ItemsControl
uses). You would create the ContentControl
in code-behind whenever a new item is added, and dispose of the control if an item is removed.
You can even add an ItemTemplate
property to the class to allow for a dynamic DataTemplate
- or you build the template in code-behind if you want to restrict it.
You would keep track of which Location
belongs to which ContentControl
using a Dictionary<Location, ContentControl>
. If you need two-way lookup, you can have a second Dictionary<ContentControl, Location>
or just use a single List<(Location, ContentControl)>
.
You will need to override GetVisualChild and VisualChildrenCount as shown in the linked documentation. You will add/remove the ContentControl
s to/from the custom control by calling AddVisualChild and RemoveVisualChild (an example can be seen in the linked documentation).
Layout Methods
The full rundown on the WPF layout system can be found here: MSDN - Layout
Now we get to the important bit, actually laying out the Location
items. When WPF is ready to show your control, or when the available space changes, it will call your MeasureOverride and ArrangeOverride methods.
In MeasureOverride
, you'll need to do the following:
Find the longest row and the longest column to determine "base size"
You'll need to loop through each item for this. We're look for longest as in visual size, not number of items.
At this stage, we don't know the scale of the items yet, so we measure in terms of aspect ratio. The items in your question appear to be 4:3 when landscape and 3:4 when portrait, so I'll use that. This means when calculating the length of a row, you add a4
for every landscape item, and a3
for every portrait item. For columns it's the reverse, since we're measuring height instead of width.
Create a Size variable whereWidth
is the the length of the longest row andHeight
is the length of the longest column. This will be calledbaseSize
- its the size of the control before any scaling.Compare
baseSize
withavailableSize
to determine the scaling factor
You want to get the ratio of the available width to your base width, and the availalbe height to your base height. This gets you your scaling factor. Since you want to scale uniformly and never exceedavailableSize
, you take whichever of these ratios is smallest.
//I hope this math is right, it's getting late
double scale = Math.Min(availableSize.Width / baseSize.Width, availableSize.Height / baseSize.Height);
- Calculate the size for a scaled item
Since all items are the same size, there's no scene recalculating the size for every one. Do it once to save processing power.
//This will probably be some private constant somewhere in the class
Size baseLandscape = new Size(4, 3);
Size basePortrat = new Size(baseLandscape.Height, baseLandscape.Width);
//Scaling the items to available space
Size scaledLandscape = new Size(baseLandscape.Width * scale, baseLandscape.Height * scale);
Size scaledPortrait = new Size(basePortrat.Width * scale, basePortrat.Height * scale);
Call
Measure
for each child item
Loop through theDictionary
orList
from the Item Elements section again. Take eachContentControl
and callMeasure
. PassscaledLandscape
if the item is to be displayed landscape, orscaledPortrait
if it is to be displayed portrait. This tells each item how big it is allowed to be.Return the desired size of the overall control
The last thing to do is to let WPF know how big our control wants to be. This is easy enough to get. It's just outbaseSize
from earilier, multiplied by ourscale
:
return new Size(baseSize.Width * scale, baseSize.Height * scale);
Next is ArrangeOverride
, where you actually position your newly-measured items internally. In this method, you'll need to loop through all your items one final time, in row-then-column order. The size of each item will be new be available via ContentControl.DesiredSize
.
Declare a double YOffset = 0
, you'll need it later.
For each row:
Take the height if the tallest item, that will be the row's height.
Add up the width of every item, that will be the row's width.
Take
finalSize.Width
and subtract the width of the row. This gives you the amount of extra space. Since you want to center reach row, take this number and divide it by 2. We'll call this final numberXOffset
.Loop through each
ContentControl
in the row and callArrange
. Pass it aRect
withX = XOffset
,Y = YOffset
, andHeight
andWidth
equal theContentControl
'sDesiredSize
. After each item, incrementXOffset
by the width of that item.This row is done. Increment
YOffset
by the height of the row (from step 1) and move to the next row.
Conclusion
That's most of what you'll need. I've written more than I expected, but I like these kinds of challenges, so it was fun. I hope you give it a try instead of just using my second answer. Feel free to ask questions.
回答2:
As an alternative to my complicated, "right way" answer (which I still hope you read). You might be able to get by by just wrapping your top ItemsControl
in a ViewBox. You can use this control to visually scale the contents uniformly. I'm not sure how clean it will look, but it is definitly simpler than doing the math to calculate the correct size of each item.
来源:https://stackoverflow.com/questions/62271054/non-regular-grid-like-structure-with-flexible-sizing-of-items