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
Related
[![enter image description here][1]][1]I have folder with a series of jpgs, which are frames of video that have been converted into jpgs. I made some code that iterates through them and displays them.
I am trying to draw a green box on a JPG image from C#. The Hight, Width, XC and YC are in a XML I just access the data there for each frame. I got it to work using a bitmap but then to display it in WPF I have to convert it into a bitmapImage first. The problem is it takes way to long. I want the video to be played at 25 fps. so all the processing needs to happen under 40 ms. Right now it takes anywhere between .01 and .3 seconds to displays the new image.
Here is the code I have so far-
public void UpdateImage(string imageName, int[] boxData)
{
// imageName is the file path the image
Bitmap oldImage = new Bitmap(imageName);
// if objects are detected
if (boxData.Length != 0)
{
// transforms x and y cords to align box better to light
int newXC = boxData[0] - (boxData[2] / 2);
int newYC = boxData[1] - (boxData[3] / 2);
// ensures cords are not negative
if (newXC < 0)
newXC = 0;
if (newYC < 0)
newYC = 0;
// uses the DrawRectangleBorder to draw rectangles
Bitmap newImage = DrawRectangleBorder(oldImage, new Rectangle(new System.Drawing.Point(newXC, newYC), new System.Drawing.Size(boxData[2], boxData[3])), Color.Green);
// converts Bitmap to BitmapImage
using (MemoryStream memory = new MemoryStream())
{
newImage.Save(memory, ImageFormat.Png);
memory.Position = 0;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
ImportImage.Source = bitmapImage;
}
}
else
{
Bitmap newImage = oldImage;
// converts Bitmap to BitmapImage
using (MemoryStream memory = new MemoryStream())
{
newImage.Save(memory, ImageFormat.Png);
memory.Position = 0;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
ImportImage.Source = bitmapImage;
}
}
}
The DrawRectangleBorder Method-
private static Bitmap DrawRectangleBorder(Bitmap image, Rectangle rectangle, Color colour)
{
// makes new blank Bitmap from the old ones width and height
Bitmap newBitmap = new Bitmap(image.Width, image.Height);
// opens up the blank bit
using (Graphics graphics = Graphics.FromImage(newBitmap))
graphics.DrawImage(image, new Rectangle(0, 0, image.Width, image.Height),
new Rectangle(0, 0, image.Width, image.Height), GraphicsUnit.Pixel);
// what actually draws the rectangles
for (Int32 x = rectangle.Location.X; x <= rectangle.Right && x < image.Width; x++)
for (Int32 y = rectangle.Location.Y; y <= rectangle.Bottom && y < image.Height; y++)
if (y == rectangle.Location.Y || y == rectangle.Bottom || x == rectangle.Location.X || x == rectangle.Right)
newBitmap.SetPixel(x, y, colour);
return newBitmap;
}
Here is what one of the pictures look like, they are 640 by 480 resolution-
[1]: https://i.stack.imgur.com/ZiocC.jpg
Any help would be great!
You could simplify your code by using this XAML
<Canvas>
<Image x:Name="ImportImage"/>
<Path x:Name="ObjectBox"
Width="{Binding ActualWidth, ElementName=ImportImage}"
Height="{Binding ActualHeight, ElementName=ImportImage}"
Stretch="None" Stroke="Green" StrokeThickness="1"/>
</Canvas>
with an UpdateImage method like this:
public void UpdateImage(string imagePath, int[] boxData)
{
ImportImage.Source = new BitmapImage(new Uri(imagePath));
var boxes = new GeometryGroup();
for (int i = 0; i <= boxData.Length - 4; i += 4)
{
int width = boxData[i + 2];
int height = boxData[i + 3];
int x = boxData[i] - width / 2;
int y = boxData[i + 1] - height / 2;
boxes.Children.Add(new RectangleGeometry(new Rect(x, y, width, height)));
}
ObjectBox.Data = boxes;
}
here is a rough demo of using a rectangle overlaid in front of an image and controlling its position and size. Sorry, never shared wpf on SO before, but think this code is all you need. I tested the time and it can go well below 40 ms (but I am not updating the image at all, just the rectangle overlay. In case it is needed, I also found this Fast Video Display WPF but ya, did not implement it or test it out.
".cs"
namespace ImageStreamer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
Stopwatch stopWatch = new Stopwatch();
long lastTime = 0;
public MainWindow()
{
InitializeComponent();
dispatcherTimer.Tick += dispatcherTimer_Tick;
dispatcherTimer.Interval = new TimeSpan(0, 0, 0,0,25);
stopWatch.Start();
}
private void dispatcherTimer_Tick(object sender, EventArgs e)
{
targetRect.Margin = new Thickness(targetRect.Margin.Left+1, targetRect.Margin.Top+1,0,0);
targetRect.Width += 1;
targetRect.Height += 1;
Trace.WriteLine(lastTime - stopWatch.ElapsedMilliseconds);
lastTime = stopWatch.ElapsedMilliseconds;
}
private void Grid_Initialized(object sender, EventArgs e)
{
}
private void Button_Click(object sender, RoutedEventArgs e)
{
dispatcherTimer.Start();
}
}
}
"xaml"
<Window x:Class="ImageStreamer.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:local="clr-namespace:ImageStreamer"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Initialized="Grid_Initialized">
<Image x:Name="imgBox" HorizontalAlignment="Left" Height="265" Margin="166,87,0,0" VerticalAlignment="Top" Width="512" Source="/ZiocC.jpg"/>
<Rectangle x:Name="targetRect" HorizontalAlignment="Left" Height="49" Margin="323,228,0,0" Stroke="Red" VerticalAlignment="Top" Width="113" StrokeThickness="5"/>
<Button Content="Button" HorizontalAlignment="Left" Margin="24,43,0,0" VerticalAlignment="Top" Click="Button_Click"/>
</Grid>
</Window>
I have a cropped version of an image that should appear on my screen.
Image 6Island = Image.FromFile("C:\\Users\\6Island.png");
Now the next goal is to Take an image of the screen.
Bitmap CaptureScreen()
{
var image = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);
var gfx = Graphics.FromImage(image);
gfx.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
return image;
}
Image 6Island = Image.FromFile("C:\\Users\\6Island.png");
Image currentView = CaptureScreen();
I then want to see if I can I can find the image 6Island inside the new image. And the colors may vary a tiny bit. Is there anyway to do that?
This is just sample quick and dirty and very slow, but it works. This code make a "crop" of your big bitmap and compare it with your small bitmap. If equal then percentage must be 100, if unequal then percentage lower than that. I would say, if bigger than 98%, then you found it.
private static void CompareBigAndSmallBitmaps(string fileName1, string fileName2)
{
var bmpBig = (Bitmap) Image.FromFile(fileName1);
var bmpSmall = (Bitmap) Image.FromFile(fileName2);
for (var offX = 0; offX < bmpBig.Width - bmpSmall.Width; offX++)
{
for (var offY = 0; offY < bmpBig.Height - bmpSmall.Height; offY++)
{
var percentage = CompareSmallBitmaps(bmpBig, bmpSmall, offX, offY);
if (percentage > 98.0) // define percentage of equality
{
// Aha... found something here....and exit here if you want
}
}
}
}
private static double CompareSmallBitmaps(Bitmap bmpBig, Bitmap bmpSmall, int offX, int offY)
{
var equals = 0;
for (var x = 0; x < bmpSmall.Width; x++)
{
for (var y = 0; y < bmpSmall.Height; y++)
{
var color1 = bmpBig.GetPixel(x + offX, y + offY).ToArgb();
var color2 = bmpSmall.GetPixel(x, y).ToArgb();
if (color1 == color2)
{
equals++;
}
}
}
return (Convert.ToDouble(equals)/Convert.ToDouble(bmpSmall.Width*bmpSmall.Height))*100.0;
}
I have a set of procedural images that I would like to add as billboards to my helix 3D application.
Currently my application looks as following:
public partial class _3DControl
{
HelixViewport3D hVp3D;
public _3DControl()
{
InitializeComponent();
createView();
}
public void createView()
{
hVp3D = new HelixViewport3D();
var lights = new SunLight();
lights.Altitude=40;
lights.Ambient=0.4;
this.Content = hVp3D;
hVp3D.Children.Add(lights);
this.Show();
}
public void UploadBillboard(BitmapImage im, System.Windows.Media.Media3D.Point3D position,double width,double height)
{
//create material
var mat = MaterialHelper.CreateImageMaterial(im, 0);
var bboard = new BillboardVisual3D();
bboard.Material = mat;
//set coordinates
bboard.Position = position;
bboard.Width = width;
bboard.Height = height;
//add the billboard
hVp3D.Children.Add(bboard);
}
However when I call the function to add a billboard:
HelixLinker.GetHelix().UploadBillboard(((Bitmap)e).bitmapToBitmapImage(),
new System.Windows.Media.Media3D.Point3D(0, 0, 0), 100, 100);
Then I see nothing being added, any idea what I' m doing wrong?
I also tried with the RectangleVisual3D class.
public void UploadRect(BitmapImage im, System.Windows.Media.Media3D.Point3D position, double width, double height)
{
var mat = MaterialHelper.CreateImageMaterial(im, 0);
var bboard = new RectangleVisual3D ();
bboard.Material = mat;
bboard.Width = width;
hVp3D.Children.Add(bboard);
}
Which if execuded in the same way results in a (promising) image however in this case the material appears not to be properly set.
Note: I hope that the BillboardVisual3D is the right class, I'm working on something that will allow me to put image "on the floor" so to speak, I want to have flat images that don't have a depth and allow for transparancy.
Why do you set the Opacity to 0? Then you wont see anything.
Try this:
//create material
var mat = MaterialHelper.CreateImageMaterial(im);
See the documentation
Edit:
Sorry, my bad with the string. But this ist working for me:
Code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CreateBilboard();
}
private void CreateBilboard()
{
BitmapImage im = new BitmapImage(new System.Uri(#"C:\Users\Public\Pictures\Sample Pictures\Lighthouse.jpg"));
var mat = MaterialHelper.CreateImageMaterial(im, 1);
var bboard = new BillboardVisual3D();
bboard.Material = mat;
var position = new System.Windows.Media.Media3D.Point3D(0, 0, 0);
var width = 100;
var height = 100;
//set coordinates
bboard.Position = position;
bboard.Width = width;
bboard.Height = height;
//add the billboard
viewPort.Children.Add(bboard);
}
}
XAML:
<Window x:Class="BillboardImageMaterialDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ht="clr-namespace:HelixToolkit.Wpf;assembly=HelixToolkit.Wpf"
Title="BillboardImageMaterialDemo" Height="480" Width="640">
<Grid>
<ht:HelixViewport3D x:Name="viewPort" ZoomExtentsWhenLoaded="True">
<!-- Remember to add some lights -->
<ht:SunLight/>
</ht:HelixViewport3D>
</Grid>
</Window>
Result:
I'm able to draw on the image using the below code. But my problem is, it is not a continuous line. It looks somehow broken. See below image for better understanding.
My XAML :
<Grid x:Name="Gridimage1" Grid.Column="0">
<Image Name="image1" HorizontalAlignment="Left" VerticalAlignment="Top" Stretch="Fill" >
</Image>
</Grid>
My c# code :
#region "Drawing on image"
static WriteableBitmap writeableBitmap;
static Image i;
public void DrawingOnImage() // this function will be called after image load
{
if (image1.Source != null)
{
i = new Image();
RenderOptions.SetBitmapScalingMode(image1, BitmapScalingMode.NearestNeighbor);
RenderOptions.SetEdgeMode(image1, EdgeMode.Aliased);
BitmapSource BitmapSrc = new FormatConvertedBitmap(image1.Source as BitmapSource, PixelFormats.Default, null, 0);
//writeableBitmap = new WriteableBitmap((int)image1.ActualWidth, (int)image1.ActualHeight, 96, 96, PixelFormats.Bgr32, null);
writeableBitmap = new WriteableBitmap(BitmapSrc);
image1.Source = writeableBitmap;
//image1.Stretch = Stretch.None;
image1.HorizontalAlignment = HorizontalAlignment.Left;
image1.VerticalAlignment = VerticalAlignment.Top;
i = image1;
image1.MouseMove += new MouseEventHandler(i_MouseMove);
image1.MouseLeftButtonDown +=
new MouseButtonEventHandler(i_MouseLeftButtonDown);
image1.MouseRightButtonDown +=
new MouseButtonEventHandler(i_MouseRightButtonDown);
}
}
static void i_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DrawPixel(e);
}
static void i_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
DrawPixel(e);
}
}
static void DrawPixel(MouseEventArgs e)
{
double CR = i.Source.Height / i.ActualHeight;
double RR = i.Source.Width / i.ActualWidth;
int column = (int)(e.GetPosition(i).X * RR) ;
int row = (int)(e.GetPosition(i).Y * CR);
// Reserve the back buffer for updates.
writeableBitmap.Lock();
unsafe
{
// Get a pointer to the back buffer.
int pBackBuffer = (int)writeableBitmap.BackBuffer;
// Fin d the address of the pixel to draw.
pBackBuffer += row * writeableBitmap.BackBufferStride;
pBackBuffer += column * 4;
// Compute the pixel's color.
int color_data = 255 << 16; // R
color_data |= 128 << 8; // G
color_data |= 255 << 0; // B
// Assign the color data to the pixel.
*((int*)pBackBuffer) = color_data;
}
// Specify the area of the bitmap that changed.
writeableBitmap.AddDirtyRect(new Int32Rect(column, row, 1, 1));
// Release the back buffer and make it available for display.
writeableBitmap.Unlock();
}
#endregion
Image output :
You can see the line (pink color) I have drawn. It's not a continuous line. Where am I failing?
Update :
My findings after #loxxy's Inputs.Set oldx ,oldy to zero initialy.
if (oldx == 0 && oldy == 0)
{
writeableBitmap.DrawLineAa(column, row, column, row, SelectedColor);
}
else
{
if (Math.Abs(oldx - column) > 10 || Math.Abs(oldy - row) > 10)
{
writeableBitmap.DrawLineAa(column, row, column, row, SelectedColor);
}
else
{
writeableBitmap.DrawLineAa(column, row, oldx, oldy, SelectedColor);
}
}
oldx = column;
oldy = row;
Since you are drawing it pixel by pixel, the mouse has to update the coordinates at a much faster rate.
And I believe it relates to the DPI of the mouse... And you can do nothing about that.
So instead try drawing a line. So something like this in WPF:
WriteableBitmapExtensions.DrawLine
The mouse events you receive will not return consecutive points. If you move the mouse fast, the points returned will not be next to each other. If you want to draw a line under the mouse cursor, you need to draw a line from the previous point drawn to the current point on every MouseMove or MouseButtonDown callback.
I have two Image controls on each other and I set the alpha channel of some pixels to zero, from the upper one(this is the colorful). But after I "zoom" (width the ScaleTransform), a "border" will be visible around the pixels that have set. Here is a screenshot:
Here is the code:
<Grid Name="grdPhotos">
<Image Stretch="None" Source="picture_grayscale.jpg" Name="photo1" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Image Stretch="None" Source="picture.jpg" Name="photo2" MouseLeftButtonDown="photo2_MouseLeftButtonDown" HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>
private void photo2_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var photo = photo2.Source as WriteableBitmap; // A WriteableBitmap is created before from the Source BitmapImage
for (int x = 100; x < 200; x++)
{
for (int y = 100; y < 200; y++)
{
int index = Convert.ToInt32(photo.PixelWidth * y + x);
if (index > 0 && index < photo.Pixels.Length)
SetPixelAlphaChannel(ref photo.Pixels[index], 0);
}
}
var transform = new ScaleTransform { ScaleX = 2, ScaleY = 2 };
photo1.RenderTransform = photo2.RenderTransform = transform;
}
public void SetPixelAlphaChannel(ref int pixel, byte value)
{
var color = ColorFromPixel(pixel);
if (color.A == value)
return;
color.A = value;
pixel = ColorToPixel(color);
}
private Color ColorFromPixel(int pixel)
{
var argbBytes = BitConverter.GetBytes(pixel);
return new Color { A = argbBytes[3], R = argbBytes[2], G = argbBytes[1], B = argbBytes[0] };
}
private int ColorToPixel(Color color)
{
var argbBytes = new byte[] { color.B, color.G, color.R, color.A };
return BitConverter.ToInt32(argbBytes, 0);
}
Why is this? Or how can I implement a zoom functionality without this "border"? Thanks a lot.
When you zoom an image, the pixel values will be interpolated, this will result in pixels in the border are you are observing being the result of interpolating your transparent pixels with their non transparent neighbours. Unfortunately you cannot control the interpolation behaviour of render transforms. You are going to have to do this yourself, possibly via WriteableBitmap.