In a Universal Windows app, I am trying to use a background image (from an ImageSource) and tile it across a control.
XAML
<Grid x:Name="gridBackground">
<ContentPresenter />
</Grid>
C#
void UpdateBackground(ImageSource source)
{
// ...
gridBackground.Background = new ImageBrush {
ImageSource = source,
Stretch = Stretch.None
};
}
According to MSDN, ImageBrush inherits from TileBrush. It even says:
Use for an ImageBrush include decorative effects for text, or tiled
backgrounds for controls or layout containers.
I would assume that this should tile the image, if stretching is disabled, but alas, it just draws the image in the middle of the control. I don't see any actual properties to make it tile.
In WPF, there is a TileMode property and ViewPort can be set to specify the dimensions of the tile. But this seems absent under the Universal Platform.
A previous question refers to WinRT (Windows 8), but I'm hoping for a brush based solution, rather than filling a canvas with images.
How do I tile a background image with UWP?
A previous question refers to WinRT (Windows 8), but I'm hoping for a brush based solution, rather than filling a canvas with images.
Currently, there are only two solution for showing background image in Tile mode in UWP app, the first one of which you are aware is filling a canvas.
The second one I'm using is to create a Panel and draw the image on it, this idea is derived from this article
What this method does is that it abuses the fact that we are drawing repeated sets of lines in a rectangular shape. First, it tries to draw a block at the top with the same height as our tile. Then it copies that block down until it reaches the bottom.
I've modified some code and fix some issues:
public class TiledBackground : Panel
{
public ImageSource BackgroundImage
{
get { return (ImageSource)GetValue(BackgroundImageProperty); }
set { SetValue(BackgroundImageProperty, value); }
}
// Using a DependencyProperty as the backing store for BackgroundImage. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BackgroundImageProperty =
DependencyProperty.Register("BackgroundImage", typeof(ImageSource), typeof(TiledBackground), new PropertyMetadata(null, BackgroundImageChanged));
private static void BackgroundImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TiledBackground)d).OnBackgroundImageChanged();
}
private static void DesignDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TiledBackground)d).OnDesignDataChanged();
}
private ImageBrush backgroundImageBrush = null;
private bool tileImageDataRebuildNeeded = true;
private byte[] tileImagePixels = null;
private int tileImageWidth = 0;
private int tileImageHeight = 0;
private readonly BitmapPixelFormat bitmapPixelFormat = BitmapPixelFormat.Bgra8;
private readonly BitmapTransform bitmapTransform = new BitmapTransform();
private readonly BitmapAlphaMode bitmapAlphaMode = BitmapAlphaMode.Straight;
private readonly ExifOrientationMode exifOrientationMode = ExifOrientationMode.IgnoreExifOrientation;
private readonly ColorManagementMode coloManagementMode = ColorManagementMode.ColorManageToSRgb;
public TiledBackground()
{
this.backgroundImageBrush = new ImageBrush();
this.Background = backgroundImageBrush;
this.SizeChanged += TiledBackground_SizeChanged;
}
private async void TiledBackground_SizeChanged(object sender, SizeChangedEventArgs e)
{
await this.Render((int)e.NewSize.Width, (int)e.NewSize.Height);
}
private async void OnBackgroundImageChanged()
{
tileImageDataRebuildNeeded = true;
await Render((int)this.ActualWidth, (int)this.ActualHeight);
}
private async void OnDesignDataChanged()
{
tileImageDataRebuildNeeded = true;
await Render((int)this.ActualWidth, (int)this.ActualHeight);
}
private async Task RebuildTileImageData()
{
BitmapImage image = BackgroundImage as BitmapImage;
if ((image != null) && (!DesignMode.DesignModeEnabled))
{
string imgUri = image.UriSource.OriginalString;
if (!imgUri.Contains("ms-appx:///"))
{
imgUri += "ms-appx:///";
}
var imageSource = new Uri(imgUri);
StorageFile storageFile = await StorageFile.GetFileFromApplicationUriAsync(imageSource);
using (var imageStream = await storageFile.OpenAsync(FileAccessMode.Read))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
var pixelDataProvider = await decoder.GetPixelDataAsync(this.bitmapPixelFormat, this.bitmapAlphaMode,
this.bitmapTransform, this.exifOrientationMode, this.coloManagementMode
);
this.tileImagePixels = pixelDataProvider.DetachPixelData();
this.tileImageHeight = (int)decoder.PixelHeight;
this.tileImageWidth = (int)decoder.PixelWidth;
}
}
}
private byte[] CreateBackgroud(int width, int height)
{
int bytesPerPixel = this.tileImagePixels.Length / (this.tileImageWidth * this.tileImageHeight);
byte[] data = new byte[width * height * bytesPerPixel];
int y = 0;
int fullTileInRowCount = width / tileImageWidth;
int tileRowLength = tileImageWidth * bytesPerPixel;
//Stage 1: Go line by line and create a block of our pattern
//Stop when tile image height or required height is reached
while ((y < height) && (y < tileImageHeight))
{
int tileIndex = y * tileImageWidth * bytesPerPixel;
int dataIndex = y * width * bytesPerPixel;
//Copy the whole line from tile at once
for (int i = 0; i < fullTileInRowCount; i++)
{
Array.Copy(tileImagePixels, tileIndex, data, dataIndex, tileRowLength);
dataIndex += tileRowLength;
}
//Copy the rest - if there is any
//Length will evaluate to 0 if all lines were copied without remainder
Array.Copy(tileImagePixels, tileIndex, data, dataIndex,
(width - fullTileInRowCount * tileImageWidth) * bytesPerPixel);
y++; //Next line
}
//Stage 2: Now let's copy those whole blocks from top to bottom
//If there is not enough space to copy the whole block, skip to stage 3
int rowLength = width * bytesPerPixel;
int blockLength = this.tileImageHeight * rowLength;
while (y <= (height - tileImageHeight))
{
int dataBaseIndex = y * width * bytesPerPixel;
Array.Copy(data, 0, data, dataBaseIndex, blockLength);
y += tileImageHeight;
}
//Copy the rest line by line
//Use previous lines as source
for (int row = y; row < height; row++)
Array.Copy(data, (row - tileImageHeight) * rowLength, data, row * rowLength, rowLength);
return data;
}
private async Task Render(int width, int height)
{
Stopwatch fullsw = Stopwatch.StartNew();
if (tileImageDataRebuildNeeded)
await RebuildTileImageData();
if ((height > 0) && (width > 0))
{
using (var randomAccessStream = new InMemoryRandomAccessStream())
{
Stopwatch sw = Stopwatch.StartNew();
var backgroundPixels = CreateBackgroud(width, height);
sw.Stop();
Debug.WriteLine("Background generation finished: {0} ticks - {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, randomAccessStream);
encoder.SetPixelData(this.bitmapPixelFormat, this.bitmapAlphaMode, (uint)width, (uint)height, 96, 96, backgroundPixels);
await encoder.FlushAsync();
if (this.backgroundImageBrush.ImageSource == null)
{
BitmapImage bitmapImage = new BitmapImage();
randomAccessStream.Seek(0);
bitmapImage.SetSource(randomAccessStream);
this.backgroundImageBrush.ImageSource = bitmapImage;
}
else ((BitmapImage)this.backgroundImageBrush.ImageSource).SetSource(randomAccessStream);
}
}
else this.backgroundImageBrush.ImageSource = null;
fullsw.Stop();
Debug.WriteLine("Background rendering finished: {0} ticks - {1} ms", fullsw.ElapsedTicks, fullsw.ElapsedMilliseconds);
}
}
Usage:
<Grid x:Name="rootGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<tileCtrl:TiledBackground
BackgroundImage="Assets/avatar1.png"
Width="{Binding ActualWidth, ElementName=rootGrid}" Height="{Binding ActualHeight, ElementName=rootGrid}"/>
</Grid>
Check the solution in Github
All these variants are heavy for GPU. You should make it via Composition API using BorderEffect.
var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var canvasDevice = CanvasDevice.GetSharedDevice();
var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);
var bitmap = await CanvasBitmap.LoadAsync(canvasDevice, new Uri("ms-appx:///YourProject/Assets/texture.jpg"));
var drawingSurface = graphicsDevice.CreateDrawingSurface(bitmap.Size,
DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
using (var ds = CanvasComposition.CreateDrawingSession(drawingSurface))
{
ds.Clear(Colors.Transparent);
ds.DrawImage(bitmap);
}
var surfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
surfaceBrush.Stretch = CompositionStretch.None;
var border = new BorderEffect
{
ExtendX = CanvasEdgeBehavior.Wrap,
ExtendY = CanvasEdgeBehavior.Wrap,
Source = new CompositionEffectSourceParameter("source")
};
var fxFactory = compositor.CreateEffectFactory(border);
var fxBrush = fxFactory.CreateBrush();
fxBrush.SetSourceParameter("source", surfaceBrush);
var sprite = compositor.CreateSpriteVisual();
sprite.Size = new Vector2(1000000);
sprite.Brush = fxBrush;
ElementCompositionPreview.SetElementChildVisual(YourCanvas, sprite);
I tried 1000000x1000000 size sprite and it worked with no efforts.
Win2d will throw you an exception if your size is bigger than 16386px.
Actually, it is now possible to create a custom brush (with help of Composition API and Win2D) to achieve the tiling effect. Code sample here: UWP TiledBrush
In short, you just subclass the XamlCompositionBrushBase and override it's OnConnected method:
public class TiledBrush : XamlCompositionBrushBase
{
protected override void OnConnected()
{
var surface = LoadedImageSurface.StartLoadFromUri(ImageSourceUri);
var surfaceBrush = Compositor.CreateSurfaceBrush(surface);
surfaceBrush.Stretch = CompositionStretch.None;
var borderEffect = new BorderEffect()
{
Source = new CompositionEffectSourceParameter("source"),
ExtendX = Microsoft.Graphics.Canvas.CanvasEdgeBehavior.Wrap,
ExtendY = Microsoft.Graphics.Canvas.CanvasEdgeBehavior.Wrap
};
var borderEffectFactory = Compositor.CreateEffectFactory(borderEffect);
var borderEffectBrush = borderEffectFactory.CreateBrush();
borderEffectBrush.SetSourceParameter("source", surfaceBrush);
}
}
And then use it as expected:
<Grid>
<Grid.Background>
<local:TiledBrush ImageSourceUri="Assets/Texture.jpg" />
</Grid.Background>
</Grid>
See my answer to this question:
You can tile using the Win2D library. They have sample code as well; there's a tiling sample under "effects" (EffectsSample.xaml.cs).
We have the TilesBrush in the Windows Community Toolkit:
<Border BorderBrush="Black" BorderThickness="1" VerticalAlignment="Center" HorizontalAlignment="Center" Width="400" Height="400">
<Border.Background>
<brushes:TilesBrush TextureUri="ms-appx:///Assets/BrushAssets/TileTexture.png"/>
</Border.Background>
</Border>
We also have the TileControl which allows for animations.
Commenting that when using Win2d from C# one must watch out for tmemory leakage. Also there are some subtleties if you want to dynamically change the bitmap resource.
See the answer to this question for one solution to those points: Repeating brush or tile of image in WinUI 3
The "Border" example in the WindowsCompositorSamples also shows how to do this, with rotation and scaling as well.
Link: https://github.com/microsoft/WindowsCompositionSamples/tree/master/SampleGallery/Samples/SDK%2015063/BorderPlayground
I'm trying to add some ellipces with random positions into my canvas, but i can see them on my canvas. Progmab is compilling quite saccesfull. Code:
for (int i = 0; i < FirefliesCount; ++i)
{
Firefly CurrentFirefly = new Firefly();
CurrentFirefly.Speed = Randomer.Next(1, 3);
CurrentFirefly.Body = new Ellipse();
CurrentFirefly.Body.Margin = new Thickness(Randomer.Next(10, (int)MainCanvas.Width - 10),
Randomer.Next(10, (int)MainCanvas.Height - 10),
0, 0);
CurrentFirefly.Body.Fill = Brushes.Black;
CurrentFirefly.Body.Height = MainCanvas.Height / 4;
CurrentFirefly.Body.Width = 1.5 * CurrentFirefly.Body.Height;
MainCanvas.Children.Add(CurrentFirefly.Body);
}
And Fireflie class:
class Firefly
{
public Ellipse Body { get; set; }
public int Speed { get; set; }
}
Probably you did not set the Width and Height properties of your MainCanvas; then they have the value NaN and therefore you will not see the ellipses.
My suggestion is to use ActualWidth and ActualHeight instead and to delay the adding of the ellipses until the canvas is loaded. Here is an example:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainCanvas.Loaded += MainCanvas_Loaded;
}
void MainCanvas_Loaded(object sender, RoutedEventArgs e)
{
Init();
}
private void Init()
{
const int FirefliesCount = 100;
Random Randomer = new Random();
for (int i = 0; i < FirefliesCount; ++i)
{
Firefly CurrentFirefly = new Firefly();
CurrentFirefly.Speed = Randomer.Next(1, 3);
CurrentFirefly.Body = new Ellipse();
CurrentFirefly.Body.Margin = new Thickness(Randomer.Next(10, (int)MainCanvas.ActualWidth - 10),
Randomer.Next(10, (int)MainCanvas.ActualHeight - 10),
0, 0);
CurrentFirefly.Body.Fill = Brushes.Black;
CurrentFirefly.Body.Height = MainCanvas.ActualHeight / 4;
CurrentFirefly.Body.Width = 1.5 * CurrentFirefly.Body.Height;
MainCanvas.Children.Add(CurrentFirefly.Body);
}
}
}
The corresponding xaml file looks like this:
<Window x:Class="WpfApplication7.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Canvas x:Name="MainCanvas"/>
</Window>
Hope you can help. Stuck on a simple problem for some, I'm a noob. What in trying to do is get an image objects in Silverlight/C# to drop randomly from the top of the canvas, at the moment its going right to left.
This is the from the object class.
namespace LOLWordGame
{
public class LetterA : ContentControl, IGameEntity
{
private int speed = 0;
public LetterA()
{
Image LetterImage = new Image();
LetterImage.Height = 45;
LetterImage.Width = 45;
LetterImage.Source = new BitmapImage(new Uri("images/a.png", UriKind.RelativeOrAbsolute));
this.Content = LetterImage;
Random random = new Random();
Canvas.SetLeft(this, -20);
Canvas.SetTop(this, random.Next(250, 850)); //randomly
speed = random.Next(1, 5);
}
public void Update(Canvas c)
{
Move(Direction.Down);
if (Canvas.GetLeft(this) < 100)
{
c.Children.Remove(this);
}
}
public void Move(Direction direction)
{
Canvas.SetLeft(this, Canvas.GetLeft(this) - speed);
}
}
}
Thanks in advance.
For a solution: maybe you should use the Canvase.SetTop Method instead of the SetLeft method? Hope this helps.
Secondary.. I'm sure following code is not the solution to your problem but I refactored it a bit. Try using collection initializers. You have a method Move that you only call once and the method has only one line of code: no reason to make that a method in my opinion. Also the method takes in a parameter but you do not use it inside the method.
public class LetterA : ContentControl, IGameEntity
{
private int speed = 0;
public LetterA()
{
var letterImage = new Image()
{
Height = 45,
Width = 45,
Source = new BitmapImage(new Uri("images/a.png", UriKind.RelativeOrAbsolute))
};
Content = letterImage;
var random = new Random();
Canvas.SetLeft(this, -20);
Canvas.SetTop(this, random.Next(250, 850));
speed = random.Next(1, 5);
}
public void Update(Canvas c)
{
Canvas.SetLeft(this, Canvas.GetLeft(this) - speed);
if (Canvas.GetLeft(this) < 100)
c.Children.Remove(this);
}
}
I have class, that creates Shapes for me (I tried to create some kind of "class factory" but im not sure if this is correct term for that I have created.
Problem is described in comments in my code.
public static Ellipse SomeCircle()
{
Ellipse e = new Ellipse();
double size = 10;
e.Height = size;
e.Width = size;
e.Fill = new SolidColorBrush(Colors.Orange);
e.Fill.Opacity = 0.8;
e.Stroke = new SolidColorBrush(Colors.Black);
// i want to have something like this here:
// canvas1.Children.Add(e);
// but I cant access non-static canvas1 from here
// I need this to place my ellipse in desired place
// (line below will not work if my Ellipse is not placed on canvas
// e.Margin = new Thickness(p.X - e.Width * 2, p.Y - e.Height * 2, 0, 0);
return e;
}
I have no idea how to workaround this.
I don't want to pass that canvas by parameter in my whole application...
Since you do not want to pass your Canvas around as a parameter, you could try creating an Extension Method which would act on your Canvas Object.
namespace CustomExtensions
{
public static class Shapes
{
public static Ellipse SomeCircle(this Canvas dest)
{
Ellipse e = new Ellipse();
double size = 10;
e.Height = size;
e.Width = size;
e.Fill = new SolidColorBrush(Colors.Orange);
e.Fill.Opacity = 0.8;
e.Stroke = new SolidColorBrush(Colors.Black);
dest.Children.Add(e);
return e;
}
}
}
Usage remember to add the CustomExtensions Namespace to your usings.
canvas1.SomeCircle();
My problem is that the image that I am setting to my grid is not appearing, the only thing appearing is the black background, so I know the grid is working. I am a noob, and I am very confused. Thanks for the Help :)
Code:
public partial class MainWindow : Window
{
static String ImgNameMole = "C:/Users/MonAmi/Desktop/mole2.png";
public MainWindow()
{
InitializeComponent();
GridMain();
}
private void GridMain()
{
Grid grid_Main = new Grid();
MainWindow1.Content = grid_Main;
grid_Main.Height = 350;
grid_Main.Width = 525;
grid_Main.Background = Brushes.GreenYellow;
CreateImage();
}
private Image CreateImage()
{
Image Mole = new Image();
Mole.Width = 25;
Mole.Height = 25;
ImageSource MoleImage = new BitmapImage(new Uri(ImgNameMole));
Mole.Source = MoleImage;
return Mole;
}
}
Nowhere in your code you are calling CreateImage(), so:
var img = CreateImage();
Grid.SetRow(img, 0);
Grid.SetColumn(img, 0);
grid_Main.Children.Add(img);
assuming that you have added at least one row and one column to your grid.