2010-06-09 19 views
9

Sto cercando di scrivere una classe personalizzata Panel per WPF, sovrascrivendo MeasureOverride e ArrangeOverride ma, mentre è in gran parte lavoro che sto vivendo uno strano problema che non riesco a spiegare.Un'usanza auto-sizing WPF Pannello classe

In particolare, dopo aver chiamato Arrange sugli articoli di mio figlio in ArrangeOverride dopo aver scoperto quali dovrebbero essere le dimensioni, non stanno ridimensionando le dimensioni che ho dato loro e sembrano ridimensionare le dimensioni passate al loro Metodo Measure all'interno di MeasureOverride.

Mi manca qualcosa nel modo in cui questo sistema dovrebbe funzionare? La mia comprensione è che chiamare Measure semplicemente fa in modo che il bambino valuti il ​​suo DesiredSize in base al valore disponibile fornito, e non dovrebbe influire sulla sua dimensione finale effettiva.

Ecco il mio codice completo (il Pannello, btw, ha lo scopo di organizzare i bambini nel modo più efficiente in termini di spazio, dando meno spazio alle file che non ne hanno bisogno e dividendo lo spazio rimanente in modo uniforme tra il resto-- al momento supporta solo l'orientamento verticale, ma ho intenzione di aggiungere orizzontale una volta che funziona correttamente):

Modifica: Grazie per le risposte. Li esaminerò più da vicino in un momento. Tuttavia, mi permetta di chiarire come funziona il mio algoritmo previsto poiché non l'ho spiegato.

Prima di tutto, il modo migliore per pensare a ciò che sto facendo è immaginare una griglia con ogni riga impostata su *. Questo divide lo spazio in modo uniforme. Tuttavia, in alcuni casi l'elemento di una riga potrebbe non aver bisogno di tutto questo spazio; se questo è il caso, voglio prendere qualsiasi spazio rimanente e darlo a quelle file che potrebbero utilizzare lo spazio. Se nessuna riga ha bisogno di spazio aggiuntivo, cerco semplicemente di distanziare le cose in modo uniforme (è quello che sta facendo extraSpace, è solo per quel caso).

Lo faccio in due passaggi. Il punto finale del primo passaggio è determinare la "dimensione normale" finale di una riga - vale a dire. la dimensione delle righe che verranno ridotte (data una dimensione inferiore alla dimensione desiderata). Lo faccio passando dall'elemento più piccolo a quello più grande e regolando le dimensioni normali calcolate ad ogni passaggio, aggiungendo lo spazio rimanente da ciascun piccolo oggetto a ciascun oggetto più grande successivo fino a quando non ci sono più elementi "adatti" e quindi interrotti.

Nel passaggio successivo, utilizzo questo valore normale per determinare se un articolo può essere adattato o meno, semplicemente prendendo lo Min della dimensione normale con la dimensione desiderata dell'articolo.

(ho anche cambiato il metodo anonimo a una funzione lambda per semplicità.)

Edit 2: mio algoritmo sembra funzionare grande a determinare la dimensione corretta dei bambini. Tuttavia, i bambini non stanno accettando le loro dimensioni. Ho provato Goblin's suggerito MeasureOverride passando PositiveInfinity e restituendo Size (0,0), ma questo fa sì che i bambini si disegnino come se non ci fossero limiti di spazio. La parte che non è ovvia su questo è che sta accadendo a causa di una chiamata a Measure. La documentazione di Microsoft su questo argomento non è del tutto chiara, dato che ho letto più volte su ogni classe e descrizione della proprietà. Tuttavia, ora è chiaro che chiamare Measure influenza effettivamente il rendering del figlio, quindi tenterò di suddividere la logica tra le due funzioni come suggerito da BladeWise.

Risolto !! Ho funzionato.Come sospettavo, dovevo chiamare Measure() due volte su ogni bambino (una volta per valutare DesiredSize e un secondo per dare ad ogni bambino la giusta altezza). Mi sembra strano che il layout di WPF sia progettato in un modo così strano, in cui è diviso in due passaggi, ma il passaggio Misura in realtà fa due cose: misure e dimensioni bambini e il passaggio di Arrange non fa altro posiziona fisicamente i bambini. Molto bizzarro.

Inserirò il codice di lavoro in fondo.

In primo luogo, il (rotto) codice originale:

protected override Size MeasureOverride(Size availableSize) { 
    foreach (UIElement child in Children) 
     child.Measure(availableSize); 

    return availableSize; 
} 

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) { 
    double extraSpace = 0.0; 
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(child=>child.DesiredSize.Height;); 
    double remainingSpace = finalSize.Height; 
    double normalSpace = 0.0; 
    int remainingChildren = Children.Count; 
    foreach (UIElement child in sortedChildren) { 
     normalSpace = remainingSpace/remainingChildren; 
     if (child.DesiredSize.Height < normalSpace) // if == there would be no point continuing as there would be no remaining space 
      remainingSpace -= child.DesiredSize.Height; 
     else { 
      remainingSpace = 0; 
      break; 
     } 
     remainingChildren--; 
    } 

    // this is only for cases where every child item fits (i.e. the above loop terminates normally): 
    extraSpace = remainingSpace/Children.Count; 
    double offset = 0.0; 

    foreach (UIElement child in Children) { 
     //child.Measure(new Size(finalSize.Width, normalSpace)); 
     double value = Math.Min(child.DesiredSize.Height, normalSpace) + extraSpace; 
      child.Arrange(new Rect(0, offset, finalSize.Width, value)); 
     offset += value; 
    } 

    return finalSize; 
} 

Ed ecco il codice di lavoro:

double _normalSpace = 0.0; 
double _extraSpace = 0.0; 

protected override Size MeasureOverride(Size availableSize) { 
    // first pass to evaluate DesiredSize given available size: 
    foreach (UIElement child in Children) 
     child.Measure(availableSize); 

    // now determine the "normal" size: 
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(child => child.DesiredSize.Height); 
    double remainingSpace = availableSize.Height; 
    int remainingChildren = Children.Count; 
    foreach (UIElement child in sortedChildren) { 
     _normalSpace = remainingSpace/remainingChildren; 
     if (child.DesiredSize.Height < _normalSpace) // if == there would be no point continuing as there would be no remaining space 
      remainingSpace -= child.DesiredSize.Height; 
     else { 
      remainingSpace = 0; 
      break; 
     } 
     remainingChildren--; 
    } 
    // there will be extra space if every child fits and the above loop terminates normally: 
    _extraSpace = remainingSpace/Children.Count; // divide the remaining space up evenly among all children 

    // second pass to give each child its proper available size: 
    foreach (UIElement child in Children) 
     child.Measure(new Size(availableSize.Width, _normalSpace)); 

    return availableSize; 
} 

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) { 
    double offset = 0.0; 

    foreach (UIElement child in Children) { 
     double value = Math.Min(child.DesiredSize.Height, _normalSpace) + _extraSpace; 
     child.Arrange(new Rect(0, offset, finalSize.Width, value)); 
     offset += value; 
    } 

    return finalSize; 
} 

Esso non può essere super-efficiente quello che con dover chiamare Measure due volte (e l'iterazione Children tre volte), ma funziona. Sarebbe gradita qualsiasi ottimizzazione dell'algoritmo.

+0

Ancora una cosa: io non sono preoccupato per un'eccezione di tornare 'availableSize' in' MeasureOverride' perché il pannello non ha senso all'interno di un infinito spazio come un 'ScrollViewer'. Ha senso solo quando lo spazio è confinato. – devios1

+0

Anche io ho questo problema. Qualsiasi modifica delle dimensioni all'interno di ArrangeOverride è invece solo per le clip. Non è quello che mi aspettavo leggendo la documentazione o qualsiasi libro del WPF. Inoltre, è stato necessario chiamare Measure due volte per risolvere questo problema. Grazie. –

risposta

8

Vediamo se ho ottenuto proprio come il Panel dovrebbe funzionare:

  • Occorre determinare la dimensione desiderata di ogni UIElement bambino
  • seconda su tali dimensioni, dovrebbe determinare se c'è spazio disponibile
  • Nel caso tale lo spazio esiste, ogni dimensione di UIElement deve essere regolata in modo che l'intero spazio sia riempito (es. ogni dimensione dell'elemento verrà incrementata di una porzione dello spazio rimanente)

Se ho capito bene, l'implementazione corrente non può portare a termine questo compito, dal momento che è necessario modificare la dimensione desiderata dei bambini stessi, non solo la loro la dimensione del rendering (che viene eseguita dai passaggi Misura e Disponi).

Ricordare che il passaggio Misura viene utilizzato per determinare la quantità di spazio richiesta da un UIElement, dato un vincolo di dimensione (lo standard availableSize è passato al metodo). Nel caso di un numero Panel, invoca un passaggio Measure anche sui suoi figli, ma non imposta la dimensione desiderata dei suoi figli (in altre parole, la dimensione dei figli è un input per il passaggio di misura del pannello). Come per il passaggio di Arrange, è usato per determinare il rettangolo in cui l'elemento UI sarà finalmente reso, qualunque sia la dimensione misurata.In caso di un numero Panel, invoca anche un passaggio Arrange sui propri figli, ma proprio come la misura lo passa non modifica la dimensione desiderata dei bambini (verrà semplicemente definito lo spazio di rendering).

per ottenere il comportamento desiderato:

  • Split correttamente la logica tra la misura e disporre passaggio (nel codice, tutta la logica è nel passaggio Arrange, mentre il codice utilizzato per determinare la quantità di spazio è richiesto per ogni bambino deve essere collocato nel passaggio misura)
  • Usare una corretta AttachedProperty (cioè RequiredHeight) al posto della dimensione desiderata dei bambini (non si ha il controllo sul formato del bambino a meno che non sia impostato su Auto, quindi non c'è bisogno di prendere DesiredSize)

Dal momento che non sono sicuro di aver capito lo scopo del pannello, ho scritto un esempio:

a. creare una nuova soluzione WPF (WpfApplication1) e aggiungere un nuovo file di classe (*) CustomPanel.cs

b. Aprire le CustomPanel.cs di file e incolla questo codice

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Windows.Controls; 
using System.Windows; 

namespace WpfApplication1 
{ 
public class CustomPanel : Panel 
{ 

    /// <summary> 
    /// RequiredHeight Attached Dependency Property 
    /// </summary> 
    public static readonly DependencyProperty RequiredHeightProperty = DependencyProperty.RegisterAttached("RequiredHeight", typeof(double), typeof(CustomPanel), new FrameworkPropertyMetadata((double)double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(OnRequiredHeightPropertyChanged))); 

    private static void OnRequiredHeightPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) 
    { 

    } 

    public static double GetRequiredHeight(DependencyObject d) 
    { 
    return (double)d.GetValue(RequiredHeightProperty); 
    } 

    public static void SetRequiredHeight(DependencyObject d, double value) 
    { 
    d.SetValue(RequiredHeightProperty, value); 
    } 

    private double m_ExtraSpace = 0; 

    private double m_NormalSpace = 0; 

    protected override Size MeasureOverride(Size availableSize) 
    { 
    //Measure the children... 
    foreach (UIElement child in Children) 
    child.Measure(availableSize); 

    //Sort them depending on their desired size... 
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(new Func<UIElement, double>(delegate(UIElement child) 
    { 
    return GetRequiredHeight(child); 
    })); 

    //Compute remaining space... 
    double remainingSpace = availableSize.Height; 
    m_NormalSpace = 0.0; 
    int remainingChildren = Children.Count; 
    foreach (UIElement child in sortedChildren) 
    { 
    m_NormalSpace = remainingSpace/remainingChildren; 
    double height = GetRequiredHeight(child); 
    if (height < m_NormalSpace) // if == there would be no point continuing as there would be no remaining space 
    remainingSpace -= height; 
    else 
    { 
    remainingSpace = 0; 
    break; 
    } 
    remainingChildren--; 
    } 

    //Dtermine the extra space to add to every child... 
    m_ExtraSpace = remainingSpace/Children.Count; 
    return Size.Empty; //The panel should take all the available space... 
    } 

    protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) 
    { 
    double offset = 0.0; 

    foreach (UIElement child in Children) 
    { 
    double height = GetRequiredHeight(child); 
    double value = (double.IsNaN(height) ? m_NormalSpace : Math.Min(height, m_NormalSpace)) + m_ExtraSpace; 
    child.Arrange(new Rect(0, offset, finalSize.Width, value)); 
    offset += value; 
    } 

    return finalSize; //The final size is the available size... 
    } 
} 
} 

c. Aprire il progetto MainWindow.xaml file di e incolla questo codice

<Window x:Class="WpfApplication1.MainWindow" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
xmlns:local="clr-namespace:WpfApplication1" 
Title="MainWindow" Height="350" Width="525"> 
<Grid> 
     <local:CustomPanel> 
      <Rectangle Fill="Blue" local:CustomPanel.RequiredHeight="22"/> 
      <Rectangle Fill="Red" local:CustomPanel.RequiredHeight="70"/> 
      <Rectangle Fill="Green" local:CustomPanel.RequiredHeight="10"/> 
      <Rectangle Fill="Purple" local:CustomPanel.RequiredHeight="5"/> 
      <Rectangle Fill="Yellow" local:CustomPanel.RequiredHeight="42"/> 
      <Rectangle Fill="Orange" local:CustomPanel.RequiredHeight="41"/> 
     </local:CustomPanel> 
    </Grid> 
</Window> 
+0

Grazie per la risposta - questo è un passo nella giusta direzione, penso. Sembra funzionare bene dato il RequiredHeights. Tuttavia, poiché lo scopo del pannello è quello di * auto-size *, dovendo assegnare esplicitamente ad ogni riga un tipo di altezza richiesto di sconfiggere lo scopo. Detto questo, penso che sia ancora possibile dimensionare automaticamente il pannello, potrei dover chiamare Measure() due volte su ogni bambino per farlo però (prima per valutare l'altezza desiderata, e poi per "dirlo" effettivamente a che altezza dovrebbe essere, dal momento che apparentemente questo è necessario). Lasciatemi testare questa teoria ... – devios1

+0

Ti darò la risposta in quanto questa risposta mi ha aiutato di più a trovare la soluzione! – devios1

+0

Grazie, capisco cosa vuoi ottenere, ma temo che sia piuttosto complicato, dal momento che se una dimensione figlio è impostata su "Auto", la dimensione desiderata sarà (molto probabilmente) uguale al vincolo che stai passando Metodo di misuraCiò significa che, mentre il dimensionamento automatico può funzionare per oggetti la cui dimensione è fissa, il comportamento con gli obetti dimensionati automaticamente non sarà coerente. Hai preso in considerazione la possibilità di utilizzare RequiredHeight come nell'esempio e di associarlo a una proprietà (DesiredSize?) Tramite un convertitore? In questo modo è possibile mantenere un'implementazione del pannello pulito, mentre si tenta di ottenere il dimensionamento automatico. – BladeWise

2

ho cercato di semplificare il codice un po ':

public class CustomPanel:Panel 
{ 
    protected override Size MeasureOverride(Size availableSize) 
    { 
     foreach (UIElement child in Children) 
      child.Measure(new Size(double.PositiveInfinity,double.PositiveInfinity)); 

     return new Size(0,0); 
    } 

    protected override Size ArrangeOverride(Size finalSize) 
    { 
     double remainingSpace = Math.Max(0.0,finalSize.Height - Children.Cast<UIElement>().Sum(c => c.DesiredSize.Height)); 
     var extraSpace = remainingSpace/Children.Count; 
     double offset = 0.0; 

     foreach (UIElement child in Children) 
     { 
      double height = child.DesiredSize.Height + extraSpace; 
      child.Arrange(new Rect(0, offset, finalSize.Width, height)); 
      offset += height; 
     } 

     return finalSize; 
    } 

} 

Alcune note:

  • Non dovrebbe restituire il formato disponibile in MeasureOverride - potrebbe essere infinito positivo che causerà un'eccezione . E dal momento che fondamentalmente non ti importa di quale dimensione è, basta tornare alla nuova dimensione (0,0).
  • Per quanto riguarda il tuo problema con l'altezza dei bambini - Sto pensando che abbia a che fare con i bambini reali - sono di dimensioni limitate in qualche modo tramite Stile o proprietà rispetto a HorizontalAlignment?

EDIT: versione 2.0:

public class CustomPanel : Panel 
{ 
    protected override Size MeasureOverride(Size availableSize) 
    { 
     foreach (UIElement child in Children) 
      child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); 

     return new Size(0, 0); 
    } 

    protected override Size ArrangeOverride(Size finalSize) 
    { 
     double optimumHeight = finalSize.Height/Children.Count; 
     var smallElements = Children.Cast<UIElement>().Where(c => c.DesiredSize.Height < optimumHeight); 
     double leftOverHeight = smallElements.Sum(c => optimumHeight - c.DesiredSize.Height); 
     var extraSpaceForBigElements = leftOverHeight/(Children.Count - smallElements.Count()); 
     double offset = 0.0; 

     foreach (UIElement child in Children) 
     { 
      double height = child.DesiredSize.Height < optimumHeight ? child.DesiredSize.Height : optimumHeight + extraSpaceForBigElements; 
      child.Arrange(new Rect(0, offset, finalSize.Width, height)); 
      offset += height; 
     } 

     return finalSize; 
    } 

} 
+0

Oh aspetta - Penso di aver mancato le tue intenzioni del pannello ... Come determini quali elementi ottengono lo spazio residuo? In questo momento il codice li rende tutti un po 'più alti di quanto desiderino essere. – Goblin

+0

Goblin, il tuo codice prende in considerazione solo uno scenario, ovvero che tutti gli elementi si adattano naturalmente allo spazio specificato e hanno spazio residuo. Questo è solo un caso marginale nell'uso previsto del pannello, che si aspetta che l'altezza totale di tutti gli elementi sia maggiore dell'altezza del pannello, ma si aspetta anche che alcuni bambini siano più piccoli di 1/n dello spazio totale, e divide questo spazio "vuoto" (che si presenterebbe in righe uguali) tra i bambini più alti. – devios1

+0

+1 per dare una mano. – devios1

Problemi correlati