Related
I'm finding it awfully hard to see how to simply cover a rectangular XAML element with repeating copies of a bitmap! I am using WinUI 3 with Windows App SDK. I would like to use the repeating image as a background element in my app.
It would seem to involve the composition API. Some tantalizing clues are given by Deiderik Krohls and by JetChopper ... however (a) there does not seem to be a stable released NuGet package for the required interface and (b) this seems like a very complicated way to do something that should be simple and (c) these solutions would seem to require extra work to integrate with WinUI 3 classes such as ImageSource and BitmapImage.
Any suggestions?
You can use a Direct2D effect, the Tile Effect for that. This effect is hardware accelerated. Microsoft provides a nuget called Win2D that enables that for WinUI: Microsoft.Graphics.Win2D
Once you have created a standard WinUI3 application project, add this nuget, and for this XAML:
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<canvas:CanvasControl
x:Name="myCanvas"
Width="128"
Height="128"
CreateResources="myCanvas_CreateResources"
Draw="myCanvas_Draw" />
</StackPanel>
You can display a repetition of an image with a C# code like this:
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
// handle canvas' CreateResources event for Win2D (Direct2D) resources
private void myCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
=> args.TrackAsyncAction(CreateResources(sender).AsAsyncAction());
// create all needed resources async (here a bitmap)
CanvasBitmap _canvasBitmap;
private async Task CreateResources(CanvasControl sender)
{
// this is my 32x32 image downloaded from https://i.stack.imgur.com/454HU.jpg?s=32&g=1
_canvasBitmap = await CanvasBitmap.LoadAsync(sender, #"c:\downloads\smo.jpg");
}
// handle canvas' Draw event
// check quickstart https://microsoft.github.io/Win2D/WinUI3/html/QuickStart.htm
private void myCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
// create an intermediate command list as a feed to the Direct2D effect
using var list = new CanvasCommandList(sender);
using var session = list.CreateDrawingSession();
session.DrawImage(_canvasBitmap);
// create the Direct2D effect (here Tile effect https://learn.microsoft.com/en-us/windows/win32/direct2d/tile)
using var tile = new TileEffect();
tile.Source = list;
// use image size as source rectangle
tile.SourceRectangle = _canvasBitmap.Bounds;
// draw the effect (using bitmap as input)
args.DrawingSession.DrawImage(tile);
}
}
Here is the result with my StackOverflow avatar as the bitmap source:
The image is 32x32 and the canvas is 128x128 so we have 4x4 tiles.
You can use the TilesBrush from the CommunityToolkit.
Install the CommunityToolkit.WinUI.UI.Media NuGet package and try this code:
<Window
x:Class="TileBrushes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Media"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,*">
<Border Grid.Column="0">
<TextBlock Text="No tiles" />
</Border>
<Border Grid.Column="1">
<Border.Background>
<toolkit:TilesBrush TextureUri="ms-appx:///Assets/StoreLogo.png" />
</Border.Background>
<TextBlock Text="Tiles" />
</Border>
</Grid>
</Window>
The answer from #simon-mourier was the key for me in finally getting this done.
I created a TiledContentControl that has a ContentControl in front of the tiled background, and which reloads its bitmap image when the TileUriString property is changed (e.g. due to a binding).
There are also properties TileWidth, TileHeight to control the drawn size of the tile bitmap, as well as AlignRight and AlignBottom to make the bitmap align with the right edge or bottom edge instead of the left or top edge. The alignment parameters are useful to get a seamless continuity between two TiledContentControls that are right next to each other.
I am providing this back to the community with thanks for all of the help I've gotten on various coding challenges in the past. Note: I have done some basic testing but not extensive testing.
The key nuget packages used are Microsoft.Graphics.Win2D 1.0.4 and Microsoft.WindowsAppSDK 1.2. There are some interesting coding challenges that I discuss in a comment in the code. For example the need to prevent memory leakage when subscribing to Win2D C++ events from WinUI3 C# code.
Here is TiledContentControl.xaml:
<UserControl
x:Class="Z.Framework.TiledContentControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:win2d="using:Microsoft.Graphics.Canvas.UI.Xaml"
mc:Ignorable="d"
Padding="0"
>
<Grid
RowDefinitions="*"
ColumnDefinitions="*"
>
<win2d:CanvasControl
x:Name="CanvasControl"
Grid.Row="0"
Grid.Column="0"
>
</win2d:CanvasControl>
<ContentPresenter
Name="ContentPresenter"
Grid.Row="0"
Grid.Column="0"
Background="Transparent"
Foreground="{x:Bind Foreground, Mode=OneWay}"
HorizontalContentAlignment="{x:Bind HorizontalContentAlignment, Mode=OneWay}"
VerticalContentAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Padding="{x:Bind Padding, Mode=OneWay}"
Content="{x:Bind Content, Mode=OneWay}"
>
</ContentPresenter>
</Grid>
</UserControl>
Here is TiledContentControl.xaml.cs:
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
using Windows.Foundation;
namespace Z.Framework
{
/// <summary>
/// A control that has a tiled (repeating) bitmap background behind a content control.
///
/// Setting the TileUriString will change the tiled bitmap. Setting the drawing parameters
/// (TileWidth, TileHeight, AlignRight, AlignBottom) will scale the bitmap or offset it so
/// that it is right or bottom aligned.
/// </summary>
[ContentProperty(Name="Content")]
public sealed partial class TiledContentControl : UserControl
{
#region Discussion
// There are a number of necessary objectives to achieve the Win2D tiling with post-Load updates.
// Goal: to trigger an async load-resources when a resource-related property of the control
// changes. This is accomplished by calling StartLoadingResources when the TileUriString changes.
// Goal: cancel any resource loads that are in progress when the new load is requested.
// This is done in StartNewLoadResourcesTaskAndCleanupOldTaskAsync.
// To do it, one must store the resource-loading task (LoadResourcesTask).
// Goal: to store the resources that have been loaded, and dispose them timely.
// The LoadResourcesTask contains the loaded resources in the Result property.
// They are kept around indefinitely, except if we start a new resource load task
// then any resources in the old load task are disposed. Also, when loading several
// resources, if one of the resource loads fails then we dispose of the others.
// The CanvasResourcesRecord and LoadResourcesAsync provide a generalizable way of
// storing resources in the task result.
// Goal: make sure that any exceptions from resource creation are thrown to Win2D, so that
// Win2D can handle device-lost events (which includes Win2D triggering a new CreateResources).
// It is accomplished by only throwing load-resource exceptions from the Win2d draw handler.
// Goal: prevent Draw from being called before resources are loaded. Resource loads that are
// triggered by Win2D go through the CreateResources event handler, allowing the use of
// CanvasCreateResourcesEventArgs.TrackAsyncAction which will postpone the Draw call -- not
// until the resources are loaded but at least while the load task is started. A Draw
// callback may then occur before the load completes, but then when the load completes
// it will invalidate the CanvasControl and another Draw callback will occur.
// It does not appear to be necessary from a Win2D perspective to prevent Draw calls
// while subsequent (post-CreateResources) resource loads are being done.
// Goal: to prevent memory leaks due to .NET not being able to detect the reference cycle
// between the main control and the CanvasControl. This is accomplished by only subscribing
// to CanvasControl events while the main control is loaded.
// References:
// https://microsoft.github.io/Win2D/WinUI2/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://stackoverflow.com/questions/74527783/repeating-brush-or-tile-of-image-in-winui-3-composition-api
// https://microsoft.github.io/Win2D/WinUI2/html/RefCycles.htm
// https://english.r2d2rigo.es/
// https://microsoft.github.io/Win2D/WinUI3/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://learn.microsoft.com/en-us/windows/win32/direct2d/tile
#endregion
#region ctor
public TiledContentControl()
{
this.InitializeComponent();
this.Loaded += this.TiledContentControl_Loaded; // OK, same lifetime
this.Unloaded += this.TiledContentControl_Unloaded; // OK, same lifetime
}
private void TiledContentControl_Loaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw += this.CanvasControl_Draw; // OK, matched in Unloaded
this.CanvasControl.CreateResources += this.CanvasControl_CreateResources;
}
private void TiledContentControl_Unloaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw -= this.CanvasControl_Draw;
this.CanvasControl.CreateResources -= this.CanvasControl_CreateResources;
}
#endregion
#region CanvasResourcesRecord, LoadResourcesAsync, LoadResourcesTask
private record class CanvasResourcesRecord(
CanvasBitmap TileBitmap,
CanvasImageBrush TileBrush
): IDisposable
{
public void Dispose()
{
this.TileBitmap.Dispose();
this.TileBrush.Dispose();
}
}
static private async Task<CanvasResourcesRecord> LoadResourcesAsync(CanvasControl canvasControl, string tileUriString)
{
object[] resources = new object[2];
try {
Uri tileUri = new Uri(tileUriString);
Task<CanvasBitmap> loadTileBitmap = CanvasBitmap.LoadAsync(canvasControl, tileUri).AsTask();
CanvasBitmap tileBitmap = await loadTileBitmap;
resources[0] = tileBitmap;
CanvasImageBrush tileBrush = new CanvasImageBrush(canvasControl, tileBitmap);
tileBrush.ExtendX = CanvasEdgeBehavior.Wrap;
tileBrush.ExtendY = CanvasEdgeBehavior.Wrap;
resources[1] = tileBrush;
} catch {
// Cleanup from partial/incomplete creation
foreach (object? resource in resources) {
(resource as IDisposable)?.Dispose();
}
throw;
}
canvasControl.Invalidate(); // now that resources are loaded, we trigger an async Draw.
return new CanvasResourcesRecord(
TileBitmap: (CanvasBitmap)resources[0],
TileBrush: (CanvasImageBrush)resources[1]
);
}
private Task<CanvasResourcesRecord>? LoadResourcesTask {
get { return this._loadResourcesTask; }
set { this._loadResourcesTask = value; }
}
private Task<CanvasResourcesRecord>? _loadResourcesTask;
#endregion
#region CanvasControl_CreateResources
private void CanvasControl_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
args.TrackAsyncAction(this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync().AsAsyncAction());
}
#endregion
#region StartLoadingResources, StartNewLoadResourcesTaskAndCleanupOldTaskAsync
private void StartLoadingResources()
{
if (this.CanvasControl.IsLoaded) {
Task _ = this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync();
}
}
private async Task StartNewLoadResourcesTaskAndCleanupOldTaskAsync()
{
// Start new task, if the necessary input properties are available.
string? tileUriString = this.TileUriString;
Task<CanvasResourcesRecord>? oldTask = this.LoadResourcesTask;
if (tileUriString != null) {
this.LoadResourcesTask = LoadResourcesAsync(this.CanvasControl, tileUriString);
} else {
this.LoadResourcesTask = null;
}
// Cleanup old task.
if (oldTask != null) {
oldTask.AsAsyncAction().Cancel();
try {
await oldTask;
} catch {
// ignore exceptions from the cancelled task
} finally {
if (oldTask.IsCompletedSuccessfully) {
oldTask.Result.Dispose();
}
}
}
}
#endregion
#region CanvasControl_Draw, ActuallyDraw
private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
if (!this.DrawingParameters.AreFullyDefined) { return; }
if (!this.DrawingParameters.AreValid) { throw new InvalidOperationException($"Invalid drawing parameters (typically width or height)."); }
Task<CanvasResourcesRecord>? loadResourcesTask = this.LoadResourcesTask;
if (loadResourcesTask == null) { return; }
if (loadResourcesTask.IsCompletedSuccessfully) {
CanvasResourcesRecord canvasResources = loadResourcesTask.Result;
this.ActuallyDraw( args, canvasResources);
} else if (loadResourcesTask.IsFaulted) {
// Throw exceptions to Win2D, for example DeviceLostException resulting in new CreateResoures event
loadResourcesTask.Exception?.Handle(e => throw e);
} else {
return;
}
}
private void ActuallyDraw( CanvasDrawEventArgs args, CanvasResourcesRecord canvasResources)
{
Debug.Assert(this.DrawingParameters.AreFullyDefined && this.DrawingParameters.AreValid);
Debug.Assert(this.DrawingParameters.AlignRight != null && this.DrawingParameters.AlignBottom != null);
CanvasControl canvasControl = this.CanvasControl;
float scaleX = (float)(this.DrawingParameters.TileWidth / canvasResources.TileBitmap.Bounds.Width);
float scaleY = (float)(this.DrawingParameters.TileHeight / canvasResources.TileBitmap.Bounds.Height);
float translateX = ((bool)this.DrawingParameters.AlignRight) ? (float)((canvasControl.RenderSize.Width % this.DrawingParameters.TileWidth) - this.DrawingParameters.TileWidth) : (float)0;
float translateY = ((bool)this.DrawingParameters.AlignBottom) ? (float)((canvasControl.RenderSize.Height % this.DrawingParameters.TileHeight) - this.DrawingParameters.TileHeight) : (float)0;
Matrix3x2 transform = Matrix3x2.CreateScale( scaleX, scaleY);
transform.Translation = new Vector2(translateX, translateY);
canvasResources.TileBrush.Transform = transform;
Rect rectangle = new Rect(new Point(), canvasControl.RenderSize);
args.DrawingSession.FillRectangle(rectangle, canvasResources.TileBrush);
}
#endregion
#region Content
new public UIElement? Content {
get { return (UIElement?)this.GetValue(ContentProperty); }
set { this.SetValue(ContentProperty, value); }
}
new public static DependencyProperty ContentProperty { get; } = DependencyProperty.Register(nameof(TiledContentControl.Content), typeof(UIElement), typeof(TiledContentControl), new PropertyMetadata(default(UIElement)));
#endregion
#region TileUriString
public string? TileUriString {
get { return (string?)this.GetValue(TileUriStringProperty); }
set { this.SetValue(TileUriStringProperty, value); }
}
public static readonly DependencyProperty TileUriStringProperty = DependencyProperty.Register(nameof(TiledContentControl.TileUriString), typeof(string), typeof(TiledContentControl), new PropertyMetadata(default(string), new PropertyChangedCallback(OnTileUriStringChanged)));
private static void OnTileUriStringChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl #this = (TiledContentControl)sender;
#this.StartLoadingResources();
}
#endregion
#region TileWidth, TileHeight, AlignRight, AlignBottom; OnDrawingParameterChanged, DrawingParametersRecord, DrawingParameters
public double TileWidth {
get { return (double)this.GetValue(TileWidthProperty); }
set { this.SetValue(TileWidthProperty, value); }
}
public static readonly DependencyProperty TileWidthProperty = DependencyProperty.Register(nameof(TileWidth), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public double TileHeight {
get { return (double)this.GetValue(TileHeightProperty); }
set { this.SetValue(TileHeightProperty, value); }
}
public static readonly DependencyProperty TileHeightProperty = DependencyProperty.Register(nameof(TileHeight), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignRight {
get { return (bool?)this.GetValue(AlignRightProperty); }
set { this.SetValue(AlignRightProperty, value); }
}
public static readonly DependencyProperty AlignRightProperty = DependencyProperty.Register(nameof(AlignRight), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignBottom {
get { return (bool?)this.GetValue(AlignBottomProperty); }
set { this.SetValue(AlignBottomProperty, value); }
}
public static readonly DependencyProperty AlignBottomProperty = DependencyProperty.Register(nameof(AlignBottom), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
private static void OnDrawingParameterChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl #this = (TiledContentControl)sender;
#this.DrawingParameters = new DrawingParametersRecord(#this.TileWidth, #this.TileHeight, #this.AlignRight, #this.AlignBottom);
#this.CanvasControl.Invalidate(); // trigger an async redraw using the new parameters.
}
private record struct DrawingParametersRecord(
double TileWidth,
double TileHeight,
bool? AlignRight,
bool? AlignBottom
)
{
public bool AreFullyDefined => !double.IsNaN(this.TileWidth) && !double.IsNaN(this.TileHeight) && this.AlignBottom != null && this.AlignRight != null;
public bool AreValid => this.TileWidth > 0 && this.TileHeight > 0;
}
private DrawingParametersRecord DrawingParameters { get; set; }
#endregion
}
}
How can I make a transparent tabPage? I found solutions like set both Form's BackColor and TransparencyKey to a color like Color.LimeGreen or override OnPaintBackground with a empty method but TabPage doesn't have neither TransparencyKeyproperty norOnPaintBackground` method. How can I do that?
TabControl is a native Windows component, it always draws the tab pages opaque with no built-in support for transparency. Solving this requires a little helping of out-of-the-box thinking, a tab control with transparent tab pages simply devolves to just the tabstrip being visible. All you have to do is use panels to host the controls that are now on the tab pages and make the correct one visible with the SelectedIndexChanged event.
Best to stick this in a derived class so you can still use the tab control normally at design time. Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto the form, replacing the existing one.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
class TransparentTabControl : TabControl {
private List<Panel> pages = new List<Panel>();
public void MakeTransparent() {
if (TabCount == 0) throw new InvalidOperationException();
var height = GetTabRect(0).Bottom;
// Move controls to panels
for (int tab = 0; tab < TabCount; ++tab) {
var page = new Panel {
Left = this.Left, Top = this.Top + height,
Width = this.Width, Height = this.Height - height,
BackColor = Color.Transparent,
Visible = tab == this.SelectedIndex
};
for (int ix = TabPages[tab].Controls.Count - 1; ix >= 0; --ix) {
TabPages[tab].Controls[ix].Parent = page;
}
pages.Add(page);
this.Parent.Controls.Add(page);
}
this.Height = height /* + 1 */;
}
protected override void OnSelectedIndexChanged(EventArgs e) {
base.OnSelectedIndexChanged(e);
for (int tab = 0; tab < pages.Count; ++tab) {
pages[tab].Visible = tab == SelectedIndex;
}
}
protected override void Dispose(bool disposing) {
if (disposing) foreach (var page in pages) page.Dispose();
base.Dispose(disposing);
}
}
Call the MakeTransparent() method in the form's Load event handler:
private void Form1_Load(object sender, EventArgs e) {
transparentTabControl1.MakeTransparent();
}
I am trying to implement the ability for a user to draw and refine a polygon on a Bing Map control.
I have added a shape layer, and overridden the tap events to draw a simple polygon and that all works perfectly. I would like to further refine the control so that a user can drag and drop any of Location objects of the drawn polygon to alter its shape, but I'm not having any luck.
I have tried to add a MapItemControl, bound to the Locations property of my polygon, but without success - I don't see the pins appear on the map, even though my polygon does render correctly. Presumably the Locations collection isn't notifying the UI of any changes to the collection, so I may have to maintain a separate collection for these pins.
Once I get this going, however, I still need to be able to drag and drop the pins around to modify the polygon. Has anyone implemented this on Windows 8 that could share some thoughts, please?
EDIT
I now have my pins showing the Locations collection on the map. I'd ideally like to tie these back to the Locations collection of the polygon, but beggars can't be choosers. I now just need to be able to drag the pins around.
I added a drag handle module to the polygon in order to edit each point in the polygon. This is the walk-through I used.
Drag Handle Module
This is for version 7.0. Not sure which version you are using, but it is easily adaptable.
Edit I did this in javascript/html. Just noticed you tagged this as windows 8. Curious, are you making a silver-light control for windows 8 phones?
WPF C# Solution
Here is what I came up with and it is very easy to implement, all you need to do is hook my event onto your MapPolygon object. The idea behind the code, is once you click on the polygon, we place a pushpin (which we create events so that we can drag it) at each vertice on the polygon. As we drag and drop each pushpin we adjust the polygon.Locations as applicable.
polygon.MouseDown += new MouseButtonEventHandler(polygon_MouseDownResize);
And a way to exit the resize event, I choose to just hook onto the key down and used 'esc' to stop editing, but you can do this on right click or any other event you choose to. All your doing is clearing pushPinVertices list and resetting event variables.
// === Editable polygon Events ===
private bool _shapeEdit;
public MapPolygon selectedPoly { get; set; }
public List<Pushpin> pushPinVertices { get; set; }
void polygon_MouseDownResize(object sender, MouseButtonEventArgs e)
{
if (!_shapeEdit && selectedPoly == null)
{
_shapeEdit = true;
selectedPoly = sender as MapPolygon;
pushPinVertices = new List<Pushpin>();
int i = 0;
foreach (Microsoft.Maps.MapControl.WPF.Location vertice in selectedPoly.Locations)
{
Pushpin verticeBlock = new Pushpin();
// I use a template to place a 'vertice marker' instead of a pushpin, il provide resource below
verticeBlock.Template = (ControlTemplate)Application.Current.Resources["PushPinTemplate"];
verticeBlock.Content = "vertice";
verticeBlock.Location = vertice;
verticeBlock.MouseDown += new MouseButtonEventHandler(pin_MouseDown);
verticeBlock.MouseUp += new MouseButtonEventHandler(pin_MouseUp);
myMap.Children.Add(verticeBlock);
pushPinVertices.Add(verticeBlock);
i++;
}
}
}
private void myMap_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Escape)
{
if (_shapeEdit && selectedPoly != null)
{
foreach (Pushpin p in pushPinVertices)
{
myMap.Children.Remove(p);
}
_shapeEdit = false;
selectedPoly = null;
}
}
}
// Note: I needed my window to pass bing maps the keydown event
private void Window_KeyDown(object sender, KeyEventArgs e)
{
myMap_KeyDown(sender, e);
}
// === Editable polygon Events ===
// ==== Draggable pushpin events =====
Vector _mouseToMarker;
private bool _IsPinDragging;
public Pushpin SelectedPushpin { get; set; }
void pin_MouseUp(object sender, MouseButtonEventArgs e)
{
LocationCollection locCol = new LocationCollection();
foreach (Pushpin p in pushPinVertices)
{
locCol.Add(p.Location);
}
selectedPoly.Locations = locCol;
bingMapRefresh();
_IsPinDragging = false;
SelectedPushpin = null;
}
void pin_MouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
SelectedPushpin = (Pushpin)sender;
_IsPinDragging = true;
_mouseToMarker = Point.Subtract(
myMap.LocationToViewportPoint(SelectedPushpin.Location),
e.GetPosition(myMap));
}
private void myMap_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (_IsPinDragging && SelectedPushpin != null)
{
SelectedPushpin.Location = myMap.ViewportPointToLocation(Point.Add(e.GetPosition(myMap), _mouseToMarker));
e.Handled = true;
}
}
}
// ==== Draggable pushpin events =====
// Nice little maprefresh I found online since the bingmap WPF doesnt always seem to update elements after certain event orders
private void bingMapRefresh()
{
//myMap.UpdateLayout();
var mapCenter = myMap.Center;
mapCenter.Latitude += 0.00001;
myMap.SetView(mapCenter, myMap.ZoomLevel);
}
As for the resource to overwrite the pushpin icon, I just used an ImageBrush an made a small white square. Note you may need to adjust the Margin property depending on how long / wide you make your marker. This is because generally the pushpin is anchored above the location by default.
<Application.Resources>
<ControlTemplate x:Key="PushPinTemplate">
<Grid>
<Rectangle Width="10" Height="10" Margin="0 35 0 0">
<Rectangle.Fill>
<ImageBrush ImageSource="pack://application:,,,/Images/DragIcon.gif"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
</ControlTemplate>
</Application.Resources>
I'm looking for help resizing a label's size and its font size based on its window size.
That's to say when a window resizes, the label itself adjusts it font and background box to do the same.
I know I'm triggering something on the Resize event of the form, but I don't know what. Can anyone help?
This is in C# but it might help you.
public Form1()
{
InitializeComponent();
this.Resize += new EventHandler(Form1_Resize);
label1.AutoSize = false;
label1.Size = new Size(100, 50);
label1.BackColor = Color.White;
}
void Form1_Resize(object sender, EventArgs e)
{
label1.Size = new Size(this.ClientSize.Width / 3, this.ClientSize.Height / 3);
label1.Font = new Font("Arial", this.ClientSize.Height / 20);
}
A label is set to AutoSize by default, which should take care of itself (assuming that's what you mean by 'background box').
So, all you need to do is change the font based on the new size of the form. This should do it:
private int _lastFormSize;
private void Form1_Resize(object sender, EventArgs e)
{
var bigger = GetArea(this.Size) > _lastFormSize;
float scaleFactor = bigger ? 1.1f : 0.9f;
label1.Font = new Font(label1.Font.FontFamily.Name, label1.Font.Size*scaleFactor);
_lastFormSize = GetArea(this.Size);
}
private int GetArea(Size size)
{
return size.Height*size.Width;
}
You would probably want to deterministically calculate the scale factor based on how much the area of the form has increased/decreased by, but that code should at least get you the effect.
I am making a custom ComboBox, inherited from Winforms' standard ComboBox. For my custom ComboBox, I set DrawMode to OwnerDrawFixed and DropDownStyle to DropDownList. Then I write my own OnDrawItem method. But I ended up like this:
How do I make my Custom ComboBox to look like the Standard one?
Update 1: ButtonRenderer
After searching all around, I found the ButtonRenderer class. It provides a DrawButton static/shared method which -- as the name implies -- draws the proper 3D button. I'm experimenting with it now.
Update 2: What overwrites my control?
I tried using the Graphics properties of various objects I can think of, but I always fail. Finally, I tried the Graphics of the form, and apparently something is overwriting my button.
Here's the code:
Protected Overrides Sub OnDrawItem(ByVal e As System.Windows.Forms.DrawItemEventArgs)
Dim TextToDraw As String = _DefaultText
__Brush_Window.Color = Color.FromKnownColor(KnownColor.Window)
__Brush_Disabled.Color = Color.FromKnownColor(KnownColor.GrayText)
__Brush_Enabled.Color = Color.FromKnownColor(KnownColor.WindowText)
If e.Index >= 0 Then
TextToDraw = _DataSource.ItemText(e.Index)
End If
If TextToDraw.StartsWith("---") Then TextToDraw = StrDup(3, ChrW(&H2500)) ' U+2500 is "Box Drawing Light Horizontal"
If (e.State And DrawItemState.ComboBoxEdit) > 0 Then
'ButtonRenderer.DrawButton(e.Graphics, e.Bounds, VisualStyles.PushButtonState.Default)
Else
e.DrawBackground()
End If
With e
If _IsEnabled(.Index) Then
.Graphics.DrawString(TextToDraw, Me.Font, __Brush_Enabled, .Bounds.X, .Bounds.Y)
Else
'.Graphics.FillRectangle(__Brush_Window, .Bounds)
.Graphics.DrawString(TextToDraw, Me.Font, __Brush_Disabled, .Bounds.X, .Bounds.Y)
End If
End With
TextToDraw = Nothing
ButtonRenderer.DrawButton(Me.Parent.CreateGraphics, Me.ClientRectangle, VisualStyles.PushButtonState.Default)
'MyBase.OnDrawItem(e)
End Sub
And here's the result:
Replacing Me.Parent.CreateGraphics with e.Graphics got me this:
And doing the above + replacing Me.ClientRectangle with e.Bounds got me this:
Can anyone point me whose Graphics I must use for the ButtonRenderer.DrawButton method?
PS: The bluish border is due to my using PushButtonState.Default instead of PushButtonState.Normal
I Found An Answer! (see below)
I forgot where I found the answer... I'll edit this answer when I remember.
But apparently, I need to set the Systems.Windows.Forms.ControlStyles flags. Especially the ControlStyles.UserPaint flag.
So, my New() now looks like this:
Private _ButtonArea as New Rectangle
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
MyBase.SetStyle(ControlStyles.Opaque Or ControlStyles.UserPaint, True)
MyBase.DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
MyBase.DropDownStyle = ComboBoxStyle.DropDownList
' Cache the button's modified ClientRectangle (see Note)
With _ButtonArea
.X = Me.ClientRectangle.X - 1
.Y = Me.ClientRectangle.Y - 1
.Width = Me.ClientRectangle.Width + 2
.Height = Me.ClientRectangle.Height + 2
End With
End Sub
And now I can hook into the OnPaint event:
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
If Me.DroppedDown Then
ButtonRenderer.DrawButton(Me.CreateGraphics, _ButtonArea, VisualStyles.PushButtonState.Pressed)
Else
ButtonRenderer.DrawButton(Me.CreateGraphics, _ButtonArea, VisualStyles.PushButtonState.Normal)
End If
MyBase.OnPaint(e)
End Sub
Note: Yes, the _ButtonArea rectangle must be enlarged by 1 pixel to all directions (up, down, left, right), or else there will be a 1-pixel 'perimeter' around the ButtonRenderer that shows garbage. Made me crazy for awhile until I read that I must enlarge the Control's rect for ButtonRenderer.
I had this problem myself and the reply by pepoluan got me started. I still think a few things are missing in order to get a ComboBox with looks and behavior similar to the standard ComboBox with DropDownStyle=DropDownList though.
DropDownArrow
We also need to draw the DropDownArrow. I played around with the ComboBoxRenderer, but it draws a dark border around the area of the drop down arrow so that didn't work.
My final solution was to simply draw a similar arrow and render it onto the button in the OnPaint method.
Hot Item Behavior
We also need to ensure our ComboBox has a hot item behavior similar to the standard ComboBox. I don't know of any simple and reliable method to know when a mouse is no longer above the control. Therefore I suggest using a Timer that checks at each tick whether the mouse is still over the control.
Edit
Just added a KeyUp event handler to make sure the control would update correctly when a selection was made using the keyboard. Also made a minor correction of where the text was rendered, to ensure it is more similar to the vanilla combobox' text positioning.
Below is the full code of my customized ComboBox. It allows you to display images on each item and is always rendered as in the DropDownList style, but hopefully it should be easy to accommodate the code to your own solution.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
namespace CustomControls
{
/// <summary>
/// This is a special ComboBox that each item may conatins an image.
/// </summary>
public class ImageComboBox : ComboBox
{
private static readonly Size arrowSize = new Size(18, 20);
private bool itemIsHot;
/* Since properties such as SelectedIndex and SelectedItems may change when the mouser is hovering over items in the drop down list
* we need a property that will store the item that has been selected by comitted selection so we know what to draw as the selected item.*/
private object comittedSelection;
private readonly ImgHolder dropDownArrow = ImgHolder.Create(ImageComboBox.DropDownArrow());
private Timer hotItemTimer;
public Font SelectedItemFont { get; set; }
public Padding ImageMargin { get; set; }
//
// Summary:
// Gets or sets the path of the property to use as the image for the items
// in the System.Windows.Forms.ListControl.
//
// Returns:
// A System.String representing a single property name of the System.Windows.Forms.ListControl.DataSource
// property value, or a hierarchy of period-delimited property names that resolves
// to a property name of the final data-bound object. The default is an empty string
// ("").
//
// Exceptions:
// T:System.ArgumentException:
// The specified property path cannot be resolved through the object specified by
// the System.Windows.Forms.ListControl.DataSource property.
[DefaultValue("")]
[Editor("System.Windows.Forms.Design.DataMemberFieldEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string ImageMember { get; set; }
public ImageComboBox()
{
base.SetStyle(ControlStyles.Opaque | ControlStyles.UserPaint, true);
//All the elements in the control are drawn manually.
base.DrawMode = DrawMode.OwnerDrawFixed;
//Specifies that the list is displayed by clicking the down arrow and that the text portion is not editable.
//This means that the user cannot enter a new value.
//Only values already in the list can be selected.
this.DropDownStyle = ComboBoxStyle.DropDownList;
//using DrawItem event we need to draw item
this.DrawItem += this.ComboBoxDrawItemEvent;
this.hotItemTimer = new Timer();
this.hotItemTimer.Interval = 250;
this.hotItemTimer.Tick += this.HotItemTimer_Tick;
this.MouseEnter += this.ImageComboBox_MouseEnter;
this.KeyUp += this.ImageComboBox_KeyUp;
this.SelectedItemFont = this.Font;
this.ImageMargin = new Padding(4, 4, 5, 4);
this.SelectionChangeCommitted += this.ImageComboBox_SelectionChangeCommitted;
this.SelectedIndexChanged += this.ImageComboBox_SelectedIndexChanged;
}
private static Image DropDownArrow()
{
var arrow = new Bitmap(8, 4, PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(arrow))
{
g.CompositingQuality = CompositingQuality.HighQuality;
g.FillPolygon(Brushes.Black, ImageComboBox.CreateArrowHeadPoints());
}
return arrow;
}
private static PointF[] CreateArrowHeadPoints()
{
return new PointF[4] { new PointF(0, 0), new PointF(7F, 0), new PointF(3.5F, 3.5F), new PointF(0, 0) };
}
private static void DrawComboBoxItem(Graphics g, string text, Image image, Rectangle itemArea, int itemHeight, int itemWidth, Padding imageMargin
, Brush brush, Font font)
{
if (image != null)
{
// recalculate margins so image is always approximately vertically centered
int extraImageMargin = itemHeight - image.Height;
int imageMarginTop = Math.Max(imageMargin.Top, extraImageMargin / 2);
int imageMarginBotttom = Math.Max(imageMargin.Bottom, extraImageMargin / 2);
g.DrawImage(image, itemArea.X + imageMargin.Left, itemArea.Y + imageMarginTop, itemHeight, itemHeight - (imageMarginBotttom
+ imageMarginTop));
}
const double TEXT_MARGIN_TOP_PROPORTION = 1.1;
const double TEXT_MARGIN_BOTTOM_PROPORTION = 2 - TEXT_MARGIN_TOP_PROPORTION;
int textMarginTop = (int)Math.Round((TEXT_MARGIN_TOP_PROPORTION * itemHeight - g.MeasureString(text, font).Height) / 2.0, 0);
int textMarginBottom = (int)Math.Round((TEXT_MARGIN_BOTTOM_PROPORTION * itemHeight - g.MeasureString(text, font).Height) / 2.0, 0);
//we need to draw the item as string because we made drawmode to ownervariable
g.DrawString(text, font, brush, new RectangleF(itemArea.X + itemHeight + imageMargin.Left + imageMargin.Right, itemArea.Y + textMarginTop
, itemWidth, itemHeight - textMarginBottom));
}
private string GetDistplayText(object item)
{
if (this.DisplayMember == string.Empty) { return item.ToString(); }
else
{
var display = item.GetType().GetProperty(this.DisplayMember).GetValue(item).ToString();
return display ?? item.ToString();
}
}
private Image GetImage(object item)
{
if (this.ImageMember == string.Empty) { return null; }
else { return item.GetType().GetProperty(this.ImageMember).GetValue(item) as Image; }
}
private void ImageComboBox_SelectionChangeCommitted(object sender, EventArgs e)
{
this.comittedSelection = this.Items[this.SelectedIndex];
}
private void HotItemTimer_Tick(object sender, EventArgs e)
{
if (!this.RectangleToScreen(this.ClientRectangle).Contains(Cursor.Position)) { this.TurnOffHotItem(); }
}
private void ImageComboBox_KeyUp(object sender, KeyEventArgs e)
{
this.Invalidate();
}
private void ImageComboBox_MouseEnter(object sender, EventArgs e)
{
this.TurnOnHotItem();
}
private void ImageComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (!this.DroppedDown)
{
if (this.SelectedIndex > -1) { this.comittedSelection = this.Items[this.SelectedIndex]; }
else { this.comittedSelection = null; }
}
}
private void TurnOnHotItem()
{
this.itemIsHot = true;
this.hotItemTimer.Enabled = true;
}
private void TurnOffHotItem()
{
this.itemIsHot = false;
this.hotItemTimer.Enabled = false;
this.Invalidate(this.ClientRectangle);
}
/// <summary>
/// Draws overridden items.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ComboBoxDrawItemEvent(object sender, DrawItemEventArgs e)
{
//Draw backgroud of the item
e.DrawBackground();
if (e.Index != -1)
{
Brush brush;
if (e.State.HasFlag(DrawItemState.Focus) || e.State.HasFlag(DrawItemState.Selected)) { brush = Brushes.White; }
else { brush = Brushes.Black; }
object item = this.Items[e.Index];
ImageComboBox.DrawComboBoxItem(e.Graphics, this.GetDistplayText(item), this.GetImage(item), e.Bounds, this.ItemHeight, this.DropDownWidth
, new Padding(0, 1, 5, 1), brush, this.Font);
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// define the area of the control where we will write the text
var topTextRectangle = new Rectangle(e.ClipRectangle.X - 1, e.ClipRectangle.Y - 1, e.ClipRectangle.Width + 2, e.ClipRectangle.Height + 2);
using (var controlImage = new Bitmap(e.ClipRectangle.Width, e.ClipRectangle.Height, PixelFormat.Format32bppArgb))
{
using (Graphics ctrlG = Graphics.FromImage(controlImage))
{
/* Render the control. We use ButtonRenderer and not ComboBoxRenderer because we want the control to appear with the DropDownList style. */
if (this.DroppedDown) { ButtonRenderer.DrawButton(ctrlG, topTextRectangle, PushButtonState.Pressed); }
else if (this.itemIsHot) { ButtonRenderer.DrawButton(ctrlG, topTextRectangle, PushButtonState.Hot); }
else { ButtonRenderer.DrawButton(ctrlG, topTextRectangle, PushButtonState.Normal); }
// Draw item, if any has been selected
if (this.comittedSelection != null)
{
ImageComboBox.DrawComboBoxItem(ctrlG, this.GetDistplayText(this.comittedSelection), this.GetImage(this.comittedSelection)
, topTextRectangle, this.Height, this.Width - ImageComboBox.arrowSize.Width, this.ImageMargin, Brushes.Black, this.SelectedItemFont);
}
/* Now we need to draw the arrow. If we use ComboBoxRenderer for this job, it will display a distinct border around the dropDownArrow and we don't want that. As an alternative we define the area where the arrow should be drawn, and then procede to draw it. */
var dropDownButtonArea = new RectangleF(topTextRectangle.X + topTextRectangle.Width - (ImageComboBox.arrowSize.Width
+ this.dropDownArrow.Image.Width) / 2.0F, topTextRectangle.Y + topTextRectangle.Height - (topTextRectangle.Height
+ this.dropDownArrow.Image.Height) / 2.0F, this.dropDownArrow.Image.Width, this.dropDownArrow.Image.Height);
ctrlG.DrawImage(this.dropDownArrow.Image, dropDownButtonArea);
}
if (this.Enabled) { e.Graphics.DrawImage(controlImage, 0, 0); }
else { ControlPaint.DrawImageDisabled(e.Graphics, controlImage, 0, 0, Color.Transparent); }
}
}
}
internal struct ImgHolder
{
internal Image Image
{
get
{
return this._image ?? new Bitmap(1, 1); ;
}
}
private Image _image;
internal ImgHolder(Bitmap data)
{
_image = data;
}
internal ImgHolder(Image data)
{
_image = data;
}
internal static ImgHolder Create(Image data)
{
return new ImgHolder(data);
}
internal static ImgHolder Create(Bitmap data)
{
return new ImgHolder(data);
}
}
}