Культура приложения

39

Чтобы посмотреть на все культуры в действии, создадим пример приложения Windows Presentation Foundation (WPF), которое умеет отображать все культуры и различные их свойства. На рисунке показано, как выглядит пользовательский интерфейс этого приложения в окне визуального конструктора приложений WPF в Visual Studio 2010:

Культура в WPF

Используйте следующую XAML-разметку:

<Window x:Class="CultureDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Culture Demo" Height="460" Width="700">
    <Window.Resources>
        <Style x:Key="baseControls" TargetType="{x:Type Control}">
            <Setter Property='Margin' Value='5,5,5,5' />
        </Style>
        <Style TargetType="{x:Type Label}" BasedOn="{StaticResource baseControls}" />
        <Style TargetType="{x:Type CheckBox}" BasedOn="{StaticResource baseControls}" />
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="5" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <GridSplitter HorizontalAlignment="Right" Name="gridSplitter1" Width="67" />
        <TreeView SelectedItemChanged="treeCultures_SelectedItemChanged" Margin="5" Name="treeCultures" VerticalAlignment="Bottom" />
        <Grid Grid.Column="1" Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="3*" />
                <RowDefinition Height="3*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Label Grid.Row="0" Grid.Column="0">Culture Name:</Label>
            <TextBlock Grid.Row="0" Grid.Column="1" x:Name="textCultureName" Width="100" />
            <CheckBox Grid.Row="0" Grid.Column="2" x:Name="chkIsNeutral" HorizontalAlignment="Center" Content="Is Neutral" />

            <Label Grid.Row="1" Grid.Column="0">English Name:</Label>
            <TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" x:Name="textEnglishName" />

            <Label Grid.Row="2" Grid.Column="0">Native Name:</Label>
            <TextBlock Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" x:Name="textNativeName" />

            <Label Grid.Row="3" Grid.Column="0">Default Calendar:</Label>
            <TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" x:Name="textCalendar" />

            <Label Grid.Row="4" Grid.Column="0">Optional Calendars:</Label>
            <ListBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" x:Name="listCalendars" />


            <GroupBox x:Name="groupSamples" IsEnabled="False" Header="Samples" Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Label Grid.Row="0" Grid.Column="0">Number</Label>
                    <TextBox Grid.Row="0" Grid.Column="1" x:Name="textNumber" />

                    <Label Grid.Row="1" Grid.Column="0">Full Date</Label>
                    <TextBlock Grid.Row="1" Grid.Column="1" x:Name="textDate" />

                    <Label Grid.Row="2" Grid.Column="0">Time</Label>
                    <TextBlock Grid.Row="2" Grid.Column="1" x:Name="textTime" />
                </Grid>

            </GroupBox>
            <GroupBox x:Name="groupRegion" IsEnabled="False" Header="Region Information" Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="3">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition />
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Label Grid.Row="0" Grid.Column="0" >Region</Label>
                    <TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" x:Name="textRegion" />

                    <Label Grid.Row="1" Grid.Column="0" >Currency</Label>
                    <TextBlock Grid.Row="1" Grid.Column="1" x:Name="textCurrency" />
                    <TextBlock Grid.Row="1" Grid.Column="2" x:Name="textCurrencyISO" />

                    <CheckBox Grid.Row="2" Grid.Column="1" x:Name="checkIsMetric">Is Metric</CheckBox>

                </Grid>

            </GroupBox>
        </Grid>
    </Grid>
</Window>

Во время инициализации этого приложения все доступные культуры добавляются в элемент управления типа древовидного представления (TreeView), размещенный в левой части окна приложения. Этот процесс инициализации происходит в методе AddCulturesToTree() , который вызывается в конструкторе класса окна CultureDemoWindow.

В методе AddCulturesToTree() сначала статическим методом CultureInfo.GetCultures() получается список всех культур. Передача этому методу аргумента CultureTypes.AllCultures приводит к возврату несортированного массива всех доступных культур. Далее этот массив сортируется с использованием лямбда-выражения, которое передается делегату сравнения во втором аргументе метода Array.Sort(). Затем в цикле foreach каждая из культур добавляется в древовидное представление. Для каждой из этих культур создается объект TreeViewItem, поскольку класс TreeView в WPF предусматривает использование именно этих объектов для отображения. Свойству Tag каждого из объектов TreeViewItem присваивается объект CultureInfo для обеспечения возможности получения к нему позже доступа из дерева.

Место внутри дерева, куда добавляется каждый объект TreeViewItem, зависит от типа представляемой им культуры. Если у этой культуры нет родительской культуры, производится добавление в корневой узел дерева. Для нахождения родительских культур все культуры запоминаются внутри словаря:

private void AddCulturesToTree()
        {
            var culturesByName = new Dictionary();

            // Получаем все культуры
            var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
            Array.Sort(cultures, (c1, c2) => c1.Name.CompareTo(c2.Name));

            var nodes = new TreeViewItem[cultures.Length];

            int i = 0;
            foreach (var ci in cultures)
            {
                nodes[i] = new TreeViewItem();
                nodes[i].Header = ci.DisplayName;
                nodes[i].Tag = ci;
                culturesByName.Add(ci.Name, nodes[i]);

                TreeViewItem parent;
                if (!String.IsNullOrEmpty(ci.Parent.Name) &&
                      culturesByName.TryGetValue(ci.Parent.Name, out parent))
                {
                    parent.Items.Add(nodes[i]);
                }
                else
                {
                    treeCultures.Items.Add(nodes[i]);
                }
                i++;
            }

        }

При выборе пользователем узла внутри дерева вызывается обработчик события SelectedItemChanged элемента управления TreeView. Здесь этот обработчик реализован в методе TreeCultures_SelectedItemChanged(). Перед получением из дерева объекта CultureInfo через свойство Tag соответствующего объекта TreeViewItem производится очистка всех полей вызовом метода ClearTextFields(). Затем в некоторых из этих полей устанавливаются значения с использованием свойств Name, NativeName и EnglishName объекта CultureInfo. Если CultureInfo представляет нейтральную культуру, что может быть проверено с помощью свойства IsNeutralCulture, то устанавливается и соответствующий флажок:

private void treeCultures_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            ClearTextFields();

            CultureInfo ci = (CultureInfo)((TreeViewItem)e.NewValue).Tag;

            textCultureName.Text = ci.Name;
            textNativeName.Text = ci.NativeName;
            textEnglishName.Text = ci.EnglishName;

            chkIsNeutral.IsChecked = ci.IsNeutralCulture;
            ...

Далее извлекается информация о принятом в данной культуре календаре. Свойство Calendar класса CultureInfo возвращает используемый по умолчанию объект Calendar для конкретной культуры. Поскольку у класса Calendar нет свойства, которое бы сообщало его имя, для получения имени этого класса используется метод ToString() базового класса. Из возвращаемой им строки удаляется наименование пространства имен, а результат отображается в текстовом поле textCalendar.

Поскольку в одной культуре может поддерживаться не один, а несколько календарей, с помощью свойства OptionalCalendars возвращается массив дополнительных поддерживаемых объектов Calendar. Эти дополнительные необязательные календари отображаются в окне списка listCalendars. Класс GregorianCalendar, унаследованный от базового класса Calendar, имеет дополнительное свойство CalendarType, которое предоставляет информацию о типе григорианского календаря. В зависимости от культуры в качестве типа может быть одно из значений перечисления GregorianCalendarTypes: Arabic, MiddleEastFrench, TransliteratedFrench, USEnglish или Localized. В случае григорианских календарей информация о типе тоже отображается в окне списка:

// календарь, используемый по умолчанию
            textCalendar.Text = ci.Calendar.ToString().
                  Remove(0, 21).Replace("Calendar", "");      
                  
            listCalendars.Items.Clear();
            foreach (var optCal in ci.OptionalCalendars)
            {
                var calName = new StringBuilder(50);
                calName.Append(optCal.ToString());
                calName.Remove(0, 21);
                calName.Replace("Calendar", "");

                // в случае григорианского календаря добавление информации о его типе
                GregorianCalendar gregCal = optCal as GregorianCalendar;
                if (gregCal != null)
                {
                    calName.AppendFormat(" {0}", gregCal.CalendarType.ToString());
                }
                listCalendars.Items.Add(calName.ToString());
            }

Далее производится проверка на предмет того, является ли культура специфической (т.е. не нейтральной), за счет использования в операторе if условия !ci.IsNeutralCulture. Метод ShowSamples() отображает примеры форматирования чисел и дат. Этот метод реализуется в следующем разделе кода. Метод ShowRegionlnformation() применяется для отображения информации о регионе. В случае инвариантной культуры отображаться могут только примеры форматирования чисел и дат, но не информация о регионе. Инвариантная культура не имеет отношения ни к какому реальному языку, а потому не ассоциируется ни с каким регионом.

Ниже представлен весь код данной программы:

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

namespace CultureDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            AddCulturesToTree();
        }

        private void AddCulturesToTree()
        {
            var culturesByName = new Dictionary<string, TreeViewItem>();

            // get all cultures
            var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
            Array.Sort(cultures, (c1, c2) => c1.Name.CompareTo(c2.Name));

            var nodes = new TreeViewItem[cultures.Length];

            int i = 0;
            foreach (var ci in cultures)
            {
                nodes[i] = new TreeViewItem();
                nodes[i].Header = ci.DisplayName;
                nodes[i].Tag = ci;
                culturesByName.Add(ci.Name, nodes[i]);

                TreeViewItem parent;
                if (!String.IsNullOrEmpty(ci.Parent.Name) &&
                      culturesByName.TryGetValue(ci.Parent.Name, out parent))
                {
                    parent.Items.Add(nodes[i]);
                }
                else
                {
                    treeCultures.Items.Add(nodes[i]);
                }
                i++;
            }

        }

        private void treeCultures_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            ClearTextFields();

            // get CultureInfo object from tree
            CultureInfo ci = (CultureInfo)((TreeViewItem)e.NewValue).Tag;

            textCultureName.Text = ci.Name;
            textNativeName.Text = ci.NativeName;
            textEnglishName.Text = ci.EnglishName;

            chkIsNeutral.IsChecked = ci.IsNeutralCulture;

            // default calendar
            textCalendar.Text = ci.Calendar.ToString().
                  Remove(0, 21).Replace("Calendar", "");

            // fill optional calendars
            listCalendars.Items.Clear();
            foreach (var optCal in ci.OptionalCalendars)
            {
                var calName = new StringBuilder(50);
                calName.Append(optCal.ToString());
                calName.Remove(0, 21);
                calName.Replace("Calendar", "");

                // for GregorianCalendar add type information
                GregorianCalendar gregCal = optCal as GregorianCalendar;
                if (gregCal != null)
                {
                    calName.AppendFormat(" {0}", gregCal.CalendarType.ToString());
                }
                listCalendars.Items.Add(calName.ToString());
            }
            // display number and date samples
            if (!ci.IsNeutralCulture)
            {
                groupSamples.IsEnabled = true;
                ShowSamples(ci);

                // invariant culture doesn't have a region
                if (String.Compare(ci.ThreeLetterISOLanguageName, "IVL", true) == 0)
                {
                    groupRegion.IsEnabled = false;
                }
                else
                {
                    groupRegion.IsEnabled = true;
                    ShowRegionInformation(ci.Name);
                }
            }
            else // neutral culture: no region, no number/date formatting
            {
                groupSamples.IsEnabled = false;
                groupRegion.IsEnabled = false;
            }

        }

        private void ShowSamples(CultureInfo ci)
        {
            double number = 9876543.21;
            textNumber.Text = number.ToString("N", ci);

            DateTime today = DateTime.Today;
            textDate.Text = today.ToString("D", ci);

            DateTime now = DateTime.Now;
            textTime.Text = now.ToString("T", ci);
        }

        private void ShowRegionInformation(string culture)
        {
            var ri = new RegionInfo(culture);
            textRegion.Text = ri.DisplayName;
            textCurrency.Text = ri.CurrencySymbol;
            textCurrencyISO.Text = ri.ISOCurrencySymbol;
            checkIsMetric.IsChecked = ri.IsMetric;
        }


        private void ClearTextFields()
        {
            textCultureName.Text = String.Empty;
            textNativeName.Text = String.Empty;
            textEnglishName.Text = String.Empty;

            textCalendar.Text = string.Empty;

            listCalendars.Items.Clear();

            groupSamples.IsEnabled = false;
            textNumber.Text = string.Empty;
            textDate.Text = string.Empty;
            textTime.Text = string.Empty;

            groupRegion.IsEnabled = false;
            textRegion.Text = string.Empty;
            textCurrency.Text = string.Empty;
            textCurrencyISO.Text = string.Empty;
        }
    }
}

Запустив это приложение, можно увидеть все доступные культуры в древовидном представлении в правой части окна. Выбор какой-нибудь культуры позволяет просматривать ее характеристики в левой части окна:

Все доступные культуры

Сортировка

Порядок сортировки строк зависит от культуры. Некоторые культуры имеют разные порядки сортировки. Одним из примеров является финский язык, в котором V и W трактуются одинаково. В алгоритмах, сравнивающих строки для упорядочения, по умолчанию применяется сортировка, чувствительная к культуре.

Чтобы продемонстрировать подобное поведение на примере финского порядка сортировки, далее создается небольшое консольное приложение, в котором наименования некоторых штатов США сохраняются в массиве в несортированном виде. Планируется использовать классы из пространств имен System.Collections.Generic, System.Threading и System.Globalization, поэтому все эти пространства имен должны быть обязательно объявлены. Показанный ниже метод DisplayNames() применяется для отображения элементов массива или коллекции в окне консоли:

static void DisplayNames(string title, IEnumerable e)
        {
            Console.WriteLine(title);
            foreach (string s in e)
                Console.Write(s + "—");
            Console.WriteLine();
            Console.WriteLine();
        }

В методе Main() после создания массива с названиями некоторых штатов США свойству потока CurrentCulture назначается культура Finnish, чтобы в следующем за ним вызове Array.Sort() использовался финский порядок сортировки строк. С помощью метода DisplayNames() наименования всех штатов отображаются в окне консоли:

static void Main()
        {
            string[] names = {"Alabama", "Texas", "Washington",
                              "Virginia", "Wisconsin", "Wyoming",
                              "Kentucky", "Missouri", "Utah", "Hawaii",
                              "Kansas", "Louisiana", "Alaska", "Arizona"};

            Thread.CurrentThread.CurrentCulture = new CultureInfo("fi-FI");

            Array.Sort(names);
            DisplayNames("Sorted using the Finnish culture", names);

            // Сортировка с помощью финской культуры
            Array.Sort(names, System.Collections.Comparer.DefaultInvariant);
            DisplayNames("Sorted using the invariant culture", names);
        }

После первого отображения наименований некоторых штатов в порядке финской сортировки массив сортируется снова. При желании, чтобы сортировка выполнялась независимым от культуры пользователя образом, что может быть полезным при отправке массива на сервер или его сохранении в каком-то месте, можно использовать инвариантную культуру.

Пройди тесты
Лучший чат для C# программистов