Jak utworzyć kontener WPF z zaokrąglonymi narożnikami?

114

Tworzymy aplikację XBAP, w której musimy mieć zaokrąglone rogi w różnych lokalizacjach na jednej stronie i chcielibyśmy mieć kontener WPF Rounded Corner, w którym można umieścić kilka innych elementów. Czy ktoś ma jakieś sugestie lub przykładowy kod, jak najlepiej to osiągnąć? Czy ze stylami na lub z tworzeniem niestandardowej kontrolki?

FarrEver
źródło
1
Uwaga: jeśli umieścisz pojedynczy wiersz tekstu w obramowaniu o zaokrąglonym prostokącie, starzy ludzie, tacy jak ja, spojrzą na to i pomyślą: „Przycisk Macintosha lat 80-tych!”
mjfgates
Nie masz pojęcia, jak bardzo tęsknię za Macintoshem z lat 80-tych! Myślę, że to pytanie powinno wyraźnie określać, czy przycinanie rogów jest pożądane, ponieważ wybrana odpowiedź nie przycina obramowania.
ATL_DEV

Odpowiedzi:

266

Nie potrzebujesz niestandardowej kontrolki, po prostu umieść kontener w elemencie obramowania:

<Border BorderBrush="#FF000000" BorderThickness="1" CornerRadius="8">
   <Grid/>
</Border>

Możesz zastąpić <Grid/>dowolny z kontenerów układu ...

kobusb
źródło
30
Dla dowolnego obiektu grubości (BorderThickness lub CornerRadius) można określić pojedynczą liczbę, jeśli wszystkie 4 są takie same, na przykład CornerRadius = "8".
Santiago Palladino,
3
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="8">jest odpowiednim zamiennikiem tego, nieco bardziej soczystym
Kieren Johnstone
@Patrik Deoghare, nie mylę się, kobusb jest niesamowity ... ale myślę, że zespół WPF był niesamowity, budując to i ułatwiając korzystanie z tego. (Chociaż można by argumentować, że nowoczesna platforma UI, która nie ma tego wbudowanego ... naprawdę nie jest nowoczesną platformą UI.)
cplotts
1
Nawiasem mówiąc, realizacja Border jest niezwykle pouczająca ... jeśli masz ochotę kopać pod kołdrą. Na przykład, jak używa StreamGeometry ...
cplotts
8
OK, udało mi się to przez zwiększenie grubości obramowania, ale to rozwiązanie nie przycina rogów dzieci w pojemniku. Zapobiega tylko zachodzeniu rogów na promień granicy poprzez ograniczenie wysokości i szerokości dzieci. Rozwiązanie Chrisa Cavanagha rozwiązuje ten przypadek. Niestety, miałem nadzieję, że to rozwiązanie zadziałało, bo wydaje się wydajniejsze i bardziej eleganckie.
ATL_DEV
54

Wiem, że to nie jest odpowiedź na wstępne pytanie ... ale często chcesz przyciąć wewnętrzną zawartość tej właśnie utworzonej zaokrąglonej krawędzi.

Chris Cavanagh wymyślił na to doskonały sposób .

Wypróbowałem kilka różnych podejść do tego ... i myślę, że to rządzi.

Oto xaml poniżej:

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Background="Black"
>
    <!-- Rounded yellow border -->
    <Border
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        BorderBrush="Yellow"
        BorderThickness="3"
        CornerRadius="10"
        Padding="2"
    >
        <Grid>
            <!-- Rounded mask (stretches to fill Grid) -->
            <Border
                Name="mask"
                Background="White"
                CornerRadius="7"
            />

            <!-- Main content container -->
            <StackPanel>
                <!-- Use a VisualBrush of 'mask' as the opacity mask -->
                <StackPanel.OpacityMask>
                    <VisualBrush Visual="{Binding ElementName=mask}"/>
                </StackPanel.OpacityMask>

                <!-- Any content -->
                <Image Source="http://chriscavanagh.files.wordpress.com/2006/12/chriss-blog-banner.jpg"/>
                <Rectangle
                    Height="50"
                    Fill="Red"/>
                <Rectangle
                    Height="50"
                    Fill="White"/>
                <Rectangle
                    Height="50"
                    Fill="Blue"/>
            </StackPanel>
        </Grid>
    </Border>
</Page>
cplotts
źródło
1
Formanty Blacklight ( blacklight.codeplex.com ) mają również sprytną małą kontrolkę o nazwie ClippingBorder, która pozwala również przycinać zawartość do zaokrąglonych rogów. Jedną z fajnych rzeczy w ClippingBorder jest to, że nie używa VisualBrush (który jest jednym z najbardziej kosztownych (pod względem wydajności) pędzli).
cplotts
1
Jednak właśnie przyjrzałem się implementacji ClippingBorder ... i używa 4 ContentControl (s) w swoim domyślnym ControlTemplate (po jednym dla każdego z rogów) ... więc nie jestem pewien, czy jest mniej więcej wydajniejszy niż podejście VisualBrush powyżej. Spekuluję, że może mniej wydajne.
cplotts
To powinna być wybrana odpowiedź, jeśli wymagane jest przycinanie.
ATL_DEV
1
To jedyny prawdziwy sposób na zrobienie tego! To niesamowite, że to nie jest domyślne zachowanie dla elementów wewnątrz obramowania.
eran otzap
@eranotzap Cieszę się, że podobała Ci się ta odpowiedź. Jedynym minusem jest to, że używasz VisualBrush, który jest produktem o większej wydajności. Moja druga odpowiedź poniżej pokazuje, jak uniknąć tego VisualBrush ...
cplotts
14

Po prostu musiałem to zrobić sam, więc pomyślałem, że opublikuję tutaj inną odpowiedź.

Oto inny sposób tworzenia zaokrąglonej krawędzi narożnej i przycinania jej wewnętrznej zawartości . Jest to prosty sposób przy użyciu właściwości Clip. Fajnie, jeśli chcesz uniknąć VisualBrush.

XAML:

<Border
    Width="200"
    Height="25"
    CornerRadius="11"
    Background="#FF919194"
>
    <Border.Clip>
        <RectangleGeometry
            RadiusX="{Binding CornerRadius.TopLeft, RelativeSource={RelativeSource AncestorType={x:Type Border}}}"
            RadiusY="{Binding RadiusX, RelativeSource={RelativeSource Self}}"
        >
            <RectangleGeometry.Rect>
                <MultiBinding
                    Converter="{StaticResource widthAndHeightToRectConverter}"
                >
                    <Binding
                        Path="ActualWidth"
                        RelativeSource="{RelativeSource AncestorType={x:Type Border}}"
                    />
                    <Binding
                        Path="ActualHeight"
                        RelativeSource="{RelativeSource AncestorType={x:Type Border}}"
                    />
                </MultiBinding>
            </RectangleGeometry.Rect>
        </RectangleGeometry>
    </Border.Clip>

    <Rectangle
        Width="100"
        Height="100"
        Fill="Blue"
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
    />
</Border>

Kod konwertera:

public class WidthAndHeightToRectConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double width = (double)values[0];
        double height = (double)values[1];
        return new Rect(0, 0, width, height);
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
cplotts
źródło
Bardzo fajne rozwiązanie. Tworzę szablon kontrolny dla przycisku, który potrzebuje zarówno zewnętrznej poświaty, jak i wewnętrznej poświaty w różnych stanach, co pomogło rozwiązać problem.
Quanta,
2

Implementacja rozwiązania Kobusb do kontroli granic oparta na kodzie VB.Net. Użyłem go do wypełnienia kontrolki ListBox of Button. Kontrolki Button są tworzone na podstawie rozszerzeń MEF. Każde rozszerzenie używa atrybutu ExportMetaData MEF jako opisu rozszerzenia. Rozszerzenia to obiekty wykresów VisiFire. Użytkownik naciska przycisk wybrany z listy przycisków, aby wykonać żądany wykres.

        ' Create a ListBox of Buttons, one button for each MEF charting component. 
    For Each c As Lazy(Of ICharts, IDictionary(Of String, Object)) In ext.ChartDescriptions
        Dim brdr As New Border
        brdr.BorderBrush = Brushes.Black
        brdr.BorderThickness = New Thickness(2, 2, 2, 2)
        brdr.CornerRadius = New CornerRadius(8, 8, 8, 8)
        Dim btn As New Button
        AddHandler btn.Click, AddressOf GenericButtonClick
        brdr.Child = btn
        brdr.Background = btn.Background
        btn.Margin = brdr.BorderThickness
        btn.Width = ChartsLBx.ActualWidth - 22
        btn.BorderThickness = New Thickness(0, 0, 0, 0)
        btn.Height = 22
        btn.Content = c.Metadata("Description")
        btn.Tag = c
        btn.ToolTip = "Push button to see " & c.Metadata("Description").ToString & " chart"
        Dim lbi As New ListBoxItem
        lbi.Content = brdr
        ChartsLBx.Items.Add(lbi)
    Next

Public Event Click As RoutedEventHandler

Private Sub GenericButtonClick(sender As Object, e As RoutedEventArgs)
    Dim btn As Button = DirectCast(sender, Button)
    Dim c As Lazy(Of ICharts, IDictionary(Of String, Object)) = DirectCast(btn.Tag, Lazy(Of ICharts, IDictionary(Of String, Object)))
    Dim w As Window = DirectCast(c.Value, Window)
    Dim cc As ICharts = DirectCast(c.Value, ICharts)
    c.Value.CreateChart()
    w.Show()
End Sub

<System.ComponentModel.Composition.Export(GetType(ICharts))> _
<System.ComponentModel.Composition.ExportMetadata("Description", "Data vs. Time")> _
Public Class DataTimeChart
    Implements ICharts

    Public Sub CreateChart() Implements ICharts.CreateChart
    End Sub
End Class

Public Interface ICharts
    Sub CreateChart()
End Interface

Public Class Extensibility
    Public Sub New()
        Dim catalog As New AggregateCatalog()

        catalog.Catalogs.Add(New AssemblyCatalog(GetType(Extensibility).Assembly))

        'Create the CompositionContainer with the parts in the catalog
        ChartContainer = New CompositionContainer(catalog)

        Try
            ChartContainer.ComposeParts(Me)
        Catch ex As Exception
            Console.WriteLine(ex.ToString)
        End Try
    End Sub

    ' must use Lazy otherwise instantiation of Window will hold open app. Otherwise must specify Shutdown Mode of "Shutdown on Main Window".
    <ImportMany()> _
    Public Property ChartDescriptions As IEnumerable(Of Lazy(Of ICharts, IDictionary(Of String, Object)))

End Class
BSalita
źródło
1

Jeśli próbujesz umieścić przycisk w ramce z zaokrąglonym prostokątem, powinieneś sprawdzić przykład msdn . Znalazłem to, szukając w Google obrazów problemu (zamiast tekstu). Ich obszerny prostokąt zewnętrzny można (na szczęście) łatwo usunąć.

Zwróć uwagę, że będziesz musiał ponownie zdefiniować zachowanie przycisku (ponieważ zmieniłeś ControlTemplate). Oznacza to, że będziesz musiał zdefiniować zachowanie przycisku po kliknięciu za pomocą tagu Trigger (Property = "IsPressed" Value = "true") w tagu ControlTemplate.Triggers. Mam nadzieję, że to oszczędza komuś czas, który straciłem :)

Daniel
źródło