Сохранение управляющих изображений WPF в C#
Иногда мне нужно сохранять управляющие изображения WPF с прозрачным фоном по той или иной причине. На этот раз мне нужно было сделать несколько значков, которые отображали числа. Я мог бы использовать приложение Windows Forms (это было бы намного проще), но я хотел воспользоваться некоторыми функциями WPF. Кроме того, я знал, что в конечном итоге мне придется сохранять изображения WPF в другое время.
В этом примере вы можете ввести ширину и высоту, размер шрифта и текст и сделать изображение текста нужного размера. Когда вы нажимаете кнопку Capture, программа сохраняет результат в PNG-файле.
В следующем коде показана наиболее интересная часть кода XAML программы.
1
После предварительных комментариев код определяет метки и текстовые поля, в которые вы вводите значения. Затем он определяет кнопку Capture. Это не очевидно из этого кода, если вы действительно не знаете свой XAML, но текстовые поля изменяются, когда окно становится шире или уже. Кнопка также придерживается правой части окна.
Более функциональная часть интерфейса начинается с Grid с именем grdText. Этот элемент управления содержит прямоугольник Rectangle с закругленными углами и заполнен с помощью кисти с линейным градиентом с эффектом растрового изображения с закругленным выпуском.
Сетка Grid также содержит TextBlock со скошенным растровым эффектом. Этот элемент управления содержит Run, который определяет отображаемый текст.
Оба Rectangle и TextBlock заполняют Grid, поэтому программе необходимо изменить размер Grid на также измените размеры других.
Когда вы вводите новые значения в текстовые поля, соответствующие обработчики событий TextChanged настраивают отображение. Например, следующий код показывает, как программа реагирует при вводе новой ширины.
// Обновляем ширину прямоугольника. private void txtWidth_TextChanged(object sender, TextChangedEventArgs e) { try { if (grdText != null) grdText.Width = double.Parse(txtWidth.Text); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
Этот код проверяет, была ли еще создана сетка grdText. Если это так, программа анализирует введенную ширину и устанавливает ширину сетки этой суммы. ( Rectangle и TextBlock внутри Сетка автоматически изменяется в соответствии с.)
Следующий код показывает, как программа реагирует при изменении размера шрифта.
// Изменение размера шрифта. private void txtFontSize_TextChanged(object sender, TextChangedEventArgs e) { try { if (runText != null) runText.FontSize = double.Parse(txtFontSize.Text); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
Ключевым моментом здесь является то, что код устанавливает свойство FontSize объекта runText Run, определенного XAML. (Вот почему XAML дал объекту Run имя.)
До сих пор все довольно просто. К сожалению сохранение изображения Grid при нажатии кнопки Capture немного сложнее. (Это одна из моих проблем с WPF. Это такая операция, которая в течение многих лет использовалась в программировании Windows Forms. В ранних версиях .NET это было сложнее, но с течением времени были добавлены инструменты, чтобы упростить ее. WPF начал все сначала, игнорируя уроки, которые мы узнали за многие годы программирования .NET.)
В любом случае, код WPF, который захватывает изображение, довольно запутанный и включает в себя одну очень странную проблему, которая смущает многих людей. Эта проблема заключается в том, что простая попытка отобразить изображение элемента управления будет позиционировать изображение относительно происхождения родителя. Например, предположим, что элемент управления находится в позиции (100, 200) относительно происхождения своего родителя. Затем, если вы просто попытаетесь отобразить элемент управления, изображение будет сдвинуто на 100 пикселей вправо и на 200 пикселей вниз.
В любом случае, код WPF, который захватывает изображение, довольно запутанный и включает в себя одну очень странную проблему, которая смущает многих людей. Эта проблема заключается в том, что простая попытка отобразить изображение элемента управления будет позиционировать изображение относительно происхождения родителя. Например, предположим, что элемент управления находится в позиции (100, 200) относительно происхождения своего родителя. Затем, если вы просто попытаетесь отобразить элемент управления, изображение будет сдвинуто на 100 пикселей вправо и на 200 пикселей вниз.
...
Есть несколько способов, которыми вы можете обойти это. Один из них - переместить элемент управления в исходное состояние его родителя, отобразить его изображение и затем переместить элемент управления обратно.
Более простой подход (если вы можете применить слово «проще» ко всему, что связано с WPF и рендерингом) заключается в создании VisualBrush, который содержит изображение элемента управления. Затем вы можете заполнить целевое растровое изображение этой кистью, чтобы отобразить изображение элемента управления. Этот пример использует этот подход.
Вторая неприятная проблема заключается в том, что элемент управления отображается сам по себе, как он появляется на экране. Если он покрыт другим элементом управления, этот элемент управления отображается в рендеринге. Если элемент управления отрублен, поскольку он торчит за краем окна, его рендеринг также отрубается.
Следующий код выполняется при нажатии кнопки Capture.
// Сохраните изображение. private void btnCapture_Click(object sender, RoutedEventArgs e) { try { // Убедитесь, что окно достаточно большое. this.SizeToContent = SizeToContent.WidthAndHeight; // Сохраним файл. string filename = txtText.Text + ".png"; SaveControlImage(grdText, filename); MessageBox.Show("Done"); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
Этот код сначала устанавливает свойство SizeToContent окна для WidthAndHeight для обеспечения видимости всего элемента управления. Затем он вызывает следующий SaveControlImage метод для выполнения реальной работы.
// Сохранение образа элемента управления. private void SaveControlImage(FrameworkElement control, string filename) { // Получаем размер Visual и его потомков. Rect rect = VisualTreeHelper.GetDescendantBounds(control); // Создаем чертежVisual для создания экрана // представление элемента управления. DrawingVisual dv = new DrawingVisual(); // Заполните прямоугольник того же размера, что и элемент управления // с кистью, содержащей изображения элемента управления. using (DrawingContext ctx = dv.RenderOpen()) { VisualBrush brush = new VisualBrush(control); ctx.DrawRectangle(brush, null, new Rect(rect.Size)); } // Сделайте растровое изображение и нарисуем его. int width = (int)control.ActualWidth; int height = (int)control.ActualHeight; RenderTargetBitmap rtb = new RenderTargetBitmap( width, height, 96, 96, PixelFormats.Pbgra32); rtb.Render(dv); // Создаем кодировщик PNG. PngBitmapEncoder encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(rtb)); // Сохраним файл. using (FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) { encoder.Save(fs); } }
Этот метод сначала использует VisualTreeHelper.GetDescendantBounds, чтобы получить область, охваченную элементом управления и его потомками. Затем он создает DrawingVisual для представления ориентированного на пиксель графического объекта (в этом случае это будет растровое изображение).
Затем код создает DrawingContext, связанный с DrawingVisual. Он создает VisualBrush, который содержит изображение элемента управления. Затем он использует кисть, чтобы заполнить прямоугольник того же размера, что и элемент управления DrawingContext.
Далее код делает размер RenderTargetBitmap таким, чтобы он соответствовал фактическому размеру элемента управления. Вы можете использовать размер rect, но это, по-видимому, дает лучший результат. Затем код преобразует DrawingVisual в растровое изображение.
В этот момент метод имеет RenderTargetBitmap, удерживающий изображение элемента управления, и ему просто нужно сохранить растровое изображение в файл. Для этого код создает PngBitmapEncoder и добавляет растровое изображение в список Frames.
Наконец, метод записывает кодировщик в поток файлов для создания файла PNG.
Простой, не так ли?