Tap to focus for camera implementation - c#

I'm trying to implement a manual focus feature for my camera page so that the user can tap to focus the camera.
I'm following this StackOverflow question that's currently written in Java for native Android. I've been converting it to C# for my Xamarin.Forms Android app.
Here's what I have so far:
public class CameraPage : PageRenderer, TextureView.ISurfaceTextureListener, Android.Views.View.IOnTouchListener, IAutoFocusCallback
{
global::Android.Hardware.Camera camera;
TextureView textureView;
public void OnAutoFocus(bool success, Android.Hardware.Camera camera)
{
var parameters = camera.GetParameters();
if (parameters.FocusMode != Android.Hardware.Camera.Parameters.FocusModeContinuousPicture)
{
parameters.FocusMode = Android.Hardware.Camera.Parameters.FocusModeContinuousPicture;
if (parameters.MaxNumFocusAreas > 0)
{
parameters.FocusAreas = null;
}
camera.SetParameters(parameters);
camera.StartPreview();
}
}
public bool OnTouch(Android.Views.View v, MotionEvent e)
{
if (camera != null)
{
var parameters = camera.GetParameters();
camera.CancelAutoFocus();
Rect focusRect = CalculateTapArea(e.GetX(), e.GetY(), 1f);
if (parameters.FocusMode != Android.Hardware.Camera.Parameters.FocusModeAuto)
{
parameters.FocusMode = Android.Hardware.Camera.Parameters.FocusModeAuto;
}
if (parameters.MaxNumFocusAreas > 0)
{
List<Area> mylist = new List<Area>();
mylist.Add(new Android.Hardware.Camera.Area(focusRect, 1000));
parameters.FocusAreas = mylist;
}
try
{
camera.CancelAutoFocus();
camera.SetParameters(parameters);
camera.StartPreview();
camera.AutoFocus(OnAutoFocus); //Here is the issue. How do I use the callback?
}
catch (System.Exception ex)
{
Console.WriteLine(ex.ToString());
Console.Write(ex.StackTrace);
}
return true;
}
return false;
}
private Rect CalculateTapArea(object x, object y, float coefficient)
{
var focusAreaSize = 500;
int areaSize = Java.Lang.Float.ValueOf(focusAreaSize * coefficient).IntValue();
int left = clamp((int)x - areaSize / 2, 0, textureView.Width - areaSize);
int top = clamp((int)y - areaSize / 2, 0, textureView.Height - areaSize);
RectF rectF = new RectF(left, top, left + areaSize, top + areaSize);
Matrix.MapRect(rectF);
return new Rect((int)System.Math.Round(rectF.Left), (int)System.Math.Round(rectF.Top), (int)System.Math.Round(rectF.Right), (int)System.Math.Round(rectF.Bottom));
}
private int clamp(int x, int min, int max)
{
if (x > max)
{
return max;
}
if (x < min)
{
return min;
}
return x;
}
}
I've managed to convert most of it but I'm not sure how to properly use the AutoFocusCallback here. What should I do to call OnAutoFocus from my OnTouch event like in the java answer I linked above?
After I attached the callback, then all I need to do is subscribe the OnTouch event to my page correct or...?
For example, I tried:
textureView.Click += OnTouch; but 'no overload for 'OnTouch' matches delegate 'EventHandler'. Is there a specific event handler I need to use?

You can try change
camera.AutoFocus(OnAutoFocus);
to
camera.AutoFocus(this);
and it will be using OnAutoFocus because it implementation from IAutoFocusCallback.
And for your question about subscribe event you can try to subscribe event in OnElementChanged like this
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Page> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || Element == null)
{
return;
}
try
{
this.SetOnTouchListener(this);
}
catch (Exception e)
{
}
}
And btw I don't see to use TextureView.ISurfaceTextureListener in this code.

All that happened in the linked Java answer is that they provided the code to run when the OS calls the callback:
camera.autoFocus(new Camera.AutoFocusCallback() {
#Override
public void onAutoFocus(boolean success, Camera camera) {
camera.cancelAutoFocus();
Parameters params = camera.getParameters();
if(params.getFocusMode() != Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE){
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
camera.setParameters(params);
}
}
});
the above does not "call" the call back, just provides the call back code to run. the OS calls the call back. So in Xamarin, you need to pass in the type that is implementing the IAutoFocusCallback interface, so You should be able to do this I would think since CameraPage is implementing the IAutoFocusCallback interface:
camera.AutoFocus(this); // "this" refers to your current CameraPage which implements the interface.
the clue here is that when you type the opening parenthesis after camera.AutoFocus the popup shows that you need to pass in a type IAutoFocusCallback, which means any type that implements that interface, so in this case that is "this" CameraPage. :-)

Since there's no complete example here, here's mine.
This solution works fine, at least for me. The camera will focus continously until a focus point is tapped. It will then focus on the tap point until you move the camera away. Then it goes back to continous focus mode.
public class CameraPageRenderer : PageRenderer, TextureView.ISurfaceTextureListener, Android.Hardware.Camera.IPictureCallback, Android.Hardware.Camera.IShutterCallback, IAutoFocusCallback
{
// ... code removed for brevity
/// <summary>
/// Occurs whenever the user touches the screen. Here we set the focus mode to FocusModeAuto and set a focus area based on the tapped coordinates.
/// </summary>
public override bool OnTouchEvent(MotionEvent e)
{
var parameters = camera.GetParameters();
parameters.FocusMode = Camera.Parameters.FocusModeAuto;
if (parameters.MaxNumFocusAreas > 0)
{
var focusRect = CalculateTapArea(e.GetX(), e.GetY(), textureView.Width, textureView.Height, 50f);
parameters.FocusAreas = new List<Area>()
{
new Area(focusRect, 1000)
};
}
try
{
camera.CancelAutoFocus();
camera.SetParameters(parameters);
camera.AutoFocus(this);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
return true;
}
/// <summary>
/// Auto focus callback. Here we reset the focus mode to FocusModeContinuousPicture and remove any focus areas
/// </summary>
public void OnAutoFocus(bool success, Camera camera)
{
var parameters = camera.GetParameters();
parameters.FocusMode = Parameters.FocusModeContinuousPicture;
if (parameters.MaxNumFocusAreas > 0)
{
parameters.FocusAreas = null;
}
camera.SetParameters(parameters);
}
/// <summary>
/// Calculates a tap area using the focus coordinates mentioned in <see href="https://developer.android.com/reference/android/hardware/Camera.Parameters.html#getFocusAreas()"/>
/// <para>
/// Coordinates of the rectangle range from -1000 to 1000. (-1000, -1000) is the upper left point. (1000, 1000) is the lower right point. The width and height of focus areas cannot be 0 or negative.</para>
/// </summary>
/// <param name="x">The X coordinate of the tapped area</param>
/// <param name="y">The Y coordinate of the tapped area</param>
/// <param name="width">The total width of the tappable area</param>
/// <param name="height">The total height of the tappable area</param>
/// <param name="focusAreaSize">The desired size (widht, height) of the created rectangle</param>
/// <returns></returns>
private Rect CalculateTapArea(float x, float y, float width, float height, float focusAreaSize)
{
var leftFloat = x * 2000 / width - 1000;
var topFloat = y * 2000 / height - 1000;
var left = RoundFocusCoordinate(leftFloat);
var top = RoundFocusCoordinate(topFloat);
var right = RoundFocusCoordinate(leftFloat + focusAreaSize);
var bottom = RoundFocusCoordinate(topFloat + focusAreaSize);
return new Rect(left, top, right, bottom);
}
/// <summary>
/// Round, convert to int, and clamp between -1000 and 1000
/// </summary>
private int RoundFocusCoordinate(float value)
{
var intValue = (int)Math.Round(value, 0, MidpointRounding.AwayFromZero);
return Math.Clamp(intValue, -1000, 1000);
}
// ... code removed for brevity
}

Related

How can I draw a waveform efficiently

I am trying to display the waveform of an audio file. I would like the waveform to be drawn progressively as ffmpeg processes the file, as opposed to all at once after it's done. While I have achieved this effect, it's REALLY slow; like painfully slow. Its starts out really fast, but the speed degrades to the point of taking minutes to draw a sample.
I feel there has to be a way to do this more efficiently, as there is a program I use that does it, I just don't know how. The other program can take in >10 hours of audio and progressively display the waveform with no speed degradation. I have set ffmpeg to process the file at 500 samples/sec, but the other program samples at 1000/sec and it still runs faster than what I wrote. The other program's waveform display only takes about 120MB of RAM with a 10 hour file, where mine takes 1.5GB with a 10 minute file.
I'm fairly certain the slowness is caused by all the UI updates and the RAM usage is from all the rectangle objects being created. When I disable drawing the waveform, the async stream completes pretty fast; less than 1 min for a 10 hour file.
This is the only way I could think to accomplish what I want. I would welcome any help to improve what I wrote or any suggestions for an all together different way to accomplish it.
As a side note, this isn't all I want to display. I will eventually want to add a background grid to help judge time, and draggable line annotations to mark specific places in the waveform.
MainWindow.xaml
<ItemsControl x:Name="AudioDisplayItemsControl"
DockPanel.Dock="Top"
Height="100"
ItemsSource="{Binding Samples}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type poco:Sample}">
<Rectangle Width="{Binding Width}"
Height="{Binding Height}"
Fill="ForestGreen"/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Black"
Width="500"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
MainWindow.xaml.cs
private string _audioFilePath;
public string AudioFilePath
{
get => _audioFilePath;
set
{
if (_audioFilePath != value)
{
_audioFilePath = value;
NotifyPropertyChanged();
}
}
}
private ObservableCollection<IShape> _samples;
public ObservableCollection<IShape> Samples
{
get => _samples;
set
{
if (_samples != value)
{
_samples = value;
NotifyPropertyChanged();
}
}
}
//Eventhandler that starts this whole process
private async void GetGetWaveform_Click(object sender, RoutedEventArgs e)
{
((Button)sender).IsEnabled = false;
await GetWaveformClickAsync();
((Button)sender).IsEnabled = true;
}
private async Task GetWaveformClickAsync()
{
Samples.Clear();
double left = 0;
double width = .01;
double top = 0;
double height = 0;
await foreach (var sample in FFmpeg.GetAudioWaveform(AudioFilePath).ConfigureAwait(false))
{
// Map {-32,768, 32,767} (pcm_16le) to {-50, 50} (height of sample display)
// I don't this this mapping is correct, but that's not important right now
height = ((sample + 32768) * 100 / 65535) - 50;
// "0" pcm values are not drawn in order to save on UI updates,
// but draw position is still advanced
if (height==0)
{
left += width;
continue;
}
// Positive pcm values stretch "height" above the canvas center line
if (height > 0)
top = 50 - height;
// Negative pcm values stretch "height" below the centerline
else
{
top = 50;
height = -height;
}
Samples.Add(new Sample
{
Height = height,
Width = width,
Top = top,
Left = left,
ZIndex = 1
});
left += width;
}
}
Classes used to define a sample
public interface IShape
{
double Top { get; set; }
double Left { get; set; }
}
public abstract class Shape : IShape
{
public double Top { get; set; }
public double Left { get; set; }
public int ZIndex { get; set; }
}
public class Sample : Shape
{
public double Width { get; set; }
public double Height { get; set; }
}
FFMpeg.cs
public static class FFmpeg
{
public static async IAsyncEnumerable<short> GetAudioWaveform(string filename)
{
var args = GetFFmpegArgs(FFmpegTasks.GetWaveform, filename);
await foreach (var sample in RunFFmpegAsyncStream(args))
{
yield return sample;
}
}
/// <summary>
/// Streams raw results of running ffmpeg.exe with given arguments string
/// </summary>
/// <param name="args">CLI argument string used for ffmpeg.exe</param>
private static async IAsyncEnumerable<short> RunFFmpegAsyncStream(string args)
{
using (var process = new Process())
{
process.StartInfo.FileName = #"External\ffmpeg\bin\x64\ffmpeg.exe";
process.StartInfo.Arguments = args;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.BeginErrorReadLine();
var buffer = new byte[2];
while (true)
{
// Asynchronously read a pcm16_le value from ffmpeg.exe output
var r = await process.StandardOutput.BaseStream.ReadAsync(buffer, 0, 2);
if (r == 0)
break;
yield return BitConverter.ToInt16(buffer);
}
}
}
FFmpegTasks is just an enum.
GetFFmpegArgs uses a switch argument on FFmpegTasks to return the appropriate CLI arguments for ffmpeg.exe.
I tried using the following class instead of the standard ObservableCollection because I was hoping that less UI updates would speed things up, but it actually made drawing the waveform slower.
RangeObservableCollection.cs
public class RangeObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_suppressNotification = true;
foreach (T item in list)
{
Add(item);
}
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
You could try drawing a Path by hand. I use this to draw histograms of images in an application:
/// <summary>
/// Converts a histogram to a <see cref="PathGeometry"/>.
/// This is used by converters to draw a path.
/// It is easiest to use the default Canvas width and height and then use a ViewBox.
/// </summary>
/// <param name="histogram">The counts of each value.</param>
/// <param name="CanvasWidth">Width of the canvas</param>
/// <param name="CanvasHeight">Height of the canvas.</param>=
/// <returns>A path geometry. This value can be bound to a <see cref="Path"/>'s Data.</returns>
public static PathGeometry HistogramToPathGeometry(int[] histogram, double CanvasWidth = 100.0, double CanvasHeight = 100.0)
{
double xscale = CanvasWidth / histogram.Length;
double histMax = histogram.Max();
double yscale = CanvasHeight / histMax;
List<LineSegment> segments = new List<LineSegment>();
for (int i = 0; i < histogram.Length; i++)
{
double X = i * xscale;
double Y1 = histogram[i] * yscale;
double Y = CanvasHeight - Y1;
if (Y == double.PositiveInfinity) Y = CanvasHeight;
segments.Add(new LineSegment(new Point(X, Y), true));
}
segments.Add(new LineSegment(new Point(CanvasWidth, CanvasHeight), true));
PathGeometry geometry = new PathGeometry();
PathFigure figure = new PathFigure(new Point(0, CanvasHeight), segments, true);
geometry.Figures = new PathFigureCollection
{
figure
};
return geometry;
}
Then have this in your Xaml:
<Canvas Width="100" Height="100">
<Path Data="{Binding ConvertedPathGeometry}" />
</Canvas>
You could change this up so it handles the data as it is coming in. I'm not sure how well it would work with a lot of points, but you could only update the view after a handful of new points have come in. I've dealt with trying to draw many rectangles in a display and have the same issue you are running into.
As promised, here is all the code that is involved with drawing my waveform. I probably added a little more than necessary, but there is it. I was hoping to draw the waveform as it was processed, but gave up on that idea... for now. I hope this helps someone out there, because it took me 2 weeks in all to get this worked out. Comments are welcome.
Just as an FYI, on my pc it takes about 35 seconds to process a ~10hr m4b file and <2ms to display the image.
Also, it assumes a little-endian system. I put no checks in for big-endian systems, as I am not on one. If you are, the Buffer.BlockCopy will need some attention.
Mainwindow.xaml
<Border Background="Black"
Height="100"
Width="720"
BorderThickness="0">
<Image x:Name="WaveformImg" Source="{Binding Waveform, Mode=OneWay}"
Height="100"
Width="{Binding ImageWidth, Mode=OneWayToSource}"
Stretch="Fill" />
</Border>
<Button Content="Get Waveform"
Padding="5 0"
Margin="5 0"
Click="GetGetWaveform_Click" />
Mainwindow.xaml.cs
private async void GetGetWaveform_Click (object sender, RoutedEventArgs e)
{
((Button)sender).IsEnabled = false;
await ProcessAudiofile();
await GetWaveformClickAsync(0, AudioData!.TotalSamples - 1);
((Button)sender).IsEnabled = true;
}
private async Task ProcessAudiofile ()
{
var milliseconds = FFmpeg.GetAudioDurationSeconds(AudioFilePath);
AudioData = new AudioData(await milliseconds, FFmpeg.SampleRate, new Progress<ProgressStatus>(ReportProgress));
await AudioData.ProcessStream(FFmpeg.GetAudioWaveform(AudioFilePath)).ConfigureAwait(false);
}
private const int _IMAGE_WIDTH = 720;
private async Task GetWaveformClickAsync (int min, int max)
{
var sw = new Stopwatch();
sw.Start();
int color_ForestGreen = 0xFF << 24 | 0x22 << 16 | 0x8c << 8 | 0x22 << 0; //
Waveform = new WriteableBitmap(_IMAGE_WIDTH, 100, 96, 96, PixelFormats.Bgra32, null);
int col = 0;
int currSample = 0;
int row;
var sampleTop = 0;
var sampleBottom = 0;
var sampleHeight = 0;
//I thought this would draw the wave form line by line but it blocks the UI and draws the whole waveform at once
foreach (var sample in AudioData.GetSamples(_IMAGE_WIDTH, min, max))
{
sampleBottom = 50 + (int)(sample.min * (double)50 / short.MinValue);
sampleTop = 50 - (int)(sample.max * (double)50 / short.MaxValue);
sampleHeight = sampleBottom - sampleTop;
try
{
Waveform.Lock();
DrawLine(col, sampleTop, sampleHeight, color_ForestGreen);
col++;
}
finally
{
Waveform.Unlock();
}
}
sw.Stop();
Debug.WriteLine($"Image Creation: {sw.Elapsed}");
}
private void DrawLine (int column, int top, int height, int color)
{
unsafe
{
IntPtr pBackBuffer = Waveform.BackBuffer;
for (int i = 0; i < height; i++)
{
pBackBuffer = Waveform.BackBuffer; // Backbuffer start address
pBackBuffer += (top + i) * Waveform.BackBufferStride; // Move to address or desired row
pBackBuffer += column * 4; // Move to address of desired column
*((int*)pBackBuffer) = color;
}
}
try
{
Waveform.AddDirtyRect(new Int32Rect(column, top, 1, height));
}
catch (Exception) { } // I know this isn't a good way to deal with exceptions, but its what i did.
}
AudioData.cs
public class AudioData
{
private List<short> _amp; // pcm_s16le amplitude values
private int _expectedTotalSamples; // Number of samples expected to be returned by FFMpeg
private int _totalSamplesRead; // Current total number of samples read from the file
private IProgress<ProgressStatus>? _progressIndicator; // Communicates progress to the progress bar
private ProgressStatus _progressStatus; // Progress status to be passed to progress bar
/// <summary>
/// Total number of samples obtained from the audio file
/// </summary>
public int TotalSamples
{
get => _amp.Count;
}
/// <summary>
/// Length of audio file in seconds
/// </summary>
public double Duration
{
get => _duration;
private set
{
_duration = value < 0 ? 0 : value;
}
}
private double _duration;
/// <summary>
/// Number of data points per second of audio
/// </summary>
public int SampleRate
{
get => _sampleRate;
private set
{
_sampleRate = value < 0 ? 0 : value;
}
}
private int _sampleRate;
/// <summary>Update the window's size.</summary>
/// <param name = "duration" > How long the audio file is in milliseconds.</param>
/// <param name = "sampleRate" > Number of times per second to sample the audio file</param>
/// <param name = "progressIndicator" >Used to report progress back to a progress bar</param>
public AudioData (double duration, int sampleRate, IProgress<ProgressStatus>? progressIndicator = null)
{
Duration = duration;
SampleRate = sampleRate;
_progressIndicator = progressIndicator;
_expectedTotalSamples = (int)Math.Ceiling(Duration * SampleRate);
_amp = new List<short>();
_progressStatus = new ProgressStatus();
}
/// <summary>
/// Get values from an async pcm_s16le stream from FFMpeg
/// </summary>
/// <param name = "sampleStream" >FFMpeg samples stream</param>
public async Task ProcessStream (IAsyncEnumerable<(int read, short[] samples)> sampleStream)
{
_totalSamplesRead = 0;
_progressStatus = new ProgressStatus
{
Label = "Started",
};
await foreach ((int read, short[] samples) in sampleStream)
{
_totalSamplesRead += read;
_amp.AddRange(samples[..read]); // Only add the number of samples that where read this iteration
UpdateProgress();
}
Duration = _amp.Count() / SampleRate; // update duration to the correct value; incase duration reported by FFMpeg was wrong
UpdateProgress(true);
}
/// <summary>
/// Report progress back to the UI
/// </summary>
/// <param name="finished">Is FFmpeg done processing the file</param>
private void UpdateProgress (bool finished = false)
{
int percent = (int)(100 * (double)_totalSamplesRead / _expectedTotalSamples);
// Calculate progress update interval; once every 1%
if (percent == _progressStatus.Value)
return;
// update progress status bar object
if (finished)
{
_progressStatus.Label = "Done";
_progressStatus.Value = 100;
}
else
{
_progressStatus.Label = $"Running ({_totalSamplesRead} / {_expectedTotalSamples})";
_progressStatus.Value = percent;
}
_progressIndicator?.Report(_progressStatus);
}
/// <summary>
/// Get evenly spaced sample subsets of the entire audio file samples
/// </summary>
/// <param name="count">Number of samples to be returned</param>
/// <returns>An IEnumerable tuple containg the minimum and maximum amplitudes of a range of samples</returns>
public IEnumerable<(short min, short max)> GetSamples (int count)
{
foreach (var sample in GetSamples(count, 0, -_amp.Count - 1))
yield return sample;
}
/// <summary>
/// Get evenly spaced sample subsets of a section of the audio file samples
/// </summary>
/// <param name="count">number of data points to return</param>
/// <param name="min">inclusive starting index</param>
/// <param name="max">inclusive ending index</param>
/// <returns>An IEnumerable tuple containing the minimum and maximum amplitudes of a range of samples</returns>
public IEnumerable<(short min, short max)> GetSamples (int count, int min, int max)
{
// Protect from out of range exception
max = max >= _amp.Count ? _amp.Count - 1 : max;
max = max < 1 ? 1 : max;
min = min >= _amp.Count - 1 ? _amp.Count - 2 : min;
min = min < 0 ? 0 : min;
double sampleSize = (max - min) / (double)count; // Number of samples to inspect for return value
short rMin; // Minimum return value
short rMax; // Maximum return value
int ssOffset;
int ssLength;
for (int n = 0; n < count; n++)
{
// Calculate offset; no account for min
ssOffset = (int)(n * sampleSize);
// Determine how many samples to get, with a minimum of 1
ssLength = sampleSize <= 1 ? 1 : (int)((n + 1) * sampleSize) - ssOffset;
//shift offset to account for min
ssOffset += min;
// Double check that ssLength wont take us out of bounds
ssLength = ssOffset + ssLength >= _amp.Count ? _amp.Count - ssOffset : ssLength;
// Get the minimum and maximum amplitudes in this sample range
rMin = _amp.GetRange(ssOffset, ssLength).Min();
rMax = _amp.GetRange(ssOffset, ssLength).Max();
// In case this sample range has no (-) values, make the lowest one zero. This makes the rendered waveform look better.
rMin = rMin > 0 ? (short)0 : rMin;
rMax = rMax < 0 ? (short)0 : rMax;
yield return (rMin, rMax);
}
}
}
public class ProgressStatus
{
public int Value { get; set; }
public string Label { get; set; }
}
FFMpeg.cs
private const int _BUFER_READ_SIZE = 500; // Number of bytes to read from process.StandardOutput.BaseStream. Must be a multiple of 2
public const int SampleRate = 1000;
public static async IAsyncEnumerable<(int, short[])> GetAudioWaveform (string filename)
{
using (var process = new Process())
{
process.StartInfo = FFMpegStartInfo(FFmpegTasks.GetWaveform, filename);
process.Start();
process.BeginErrorReadLine();
await Task.Delay(1000); // Give process.StandardOutput a chance to build up values in BaseStream
var bBuffer = new byte[_BUFER_READ_SIZE]; // BaseStream.ReadAsync buffer
var sBuffer = new short[_BUFER_READ_SIZE / 2]; // Return value buffer; bBuffer.length
int read = 1; // ReadAsync returns 0 when BaseStream is empty
while (true)
{
read = await process.StandardOutput.BaseStream.ReadAsync(bBuffer, 0, _BUFER_READ_SIZE);
if (read == 0)
break;
Buffer.BlockCopy(bBuffer, 0, sBuffer, 0, read);
yield return (read / 2, sBuffer);
}
}
}
private static ProcessStartInfo FFMpegStartInfo (FFmpegTasks task, string inputFile1, string inputFile2 = "", string outputFile = "", bool overwriteOutput = true)
{
if (string.IsNullOrWhiteSpace(inputFile1))
throw new ArgumentException(nameof(inputFile1), "Path to input file is null or empty");
if (!File.Exists(inputFile1))
throw new FileNotFoundException($"No file found at: {inputFile1}", nameof(inputFile1));
var args = task switch
{
// TODO: Set appropriate sample rate
// TODO: remove -t xx
FFmpegTasks.GetWaveform => $#" -i ""{inputFile1}"" -ac 1 -filter:a aresample={SampleRate} -map 0:a -c:a pcm_s16le -f data -",
FFmpegTasks.DetectSilence => throw new NotImplementedException(),
_ => throw new NotImplementedException(),
};
return new ProcessStartInfo()
{
FileName = #"External\ffmpeg\bin\x64\ffmpeg.exe",
Arguments = args,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true
};
public enum FFmpegTasks
{
GetWaveform
}
}

How to make image zoom in & out with mouse wheel in Blazor?

I want to zoom in & out an image in blazor on asp.net.
As I use for Google Maps, I want to move the image position by zooming and dragging the image with the mouse wheel.(I want to use an image file, not a Google map.)
Is there a way to zoom in, zoom out and drag specific images in blazor?
Note:
I would only use this on BlazorWasm and not BlazorServer because there might be quite a bit of lag if the network is slow.
It is probably easier to just use JavaScript and/or JavaScript interoperability (JS interop) but for this example I decided not to use JS Interop.
This component enables you to zoom by pressing shift while mouse wheel up (zoom out) or mouse wheel down (zoom in) and move the image by pressing mouse button 1 down while moving. (it's more panning than dragging it)
Restriction in Blazor: (at the time of writing this)
The biggest issue at the moment is not having access to the mouse OffsetX and OffsetY within the html element as described here and also here, so the moving of the image has to be done using CSS only.
The reason I used Shift for scrolling is because scrolling is not being blocked or disabled as described here even with #onscroll:stopPropagation, #onwheel:stopPropagation, #onmousewheel:stopPropagation and/or #onscroll:preventDefault, #onwheel:preventDefault, #onmousewheel:preventDefault set on the parent mainImageContainer element. The screen will still scroll left and right if the content is wider than the viewable page.
Solution:
The Zooming part is pretty straight forward, all you need to do is set the transform:scale(n); property in the #onmousewheel event.
The moving of the image is a bit more complex because there is no reference to where the mouse pointer is in relation to the image or element boundaries. (OffsetX and OffsetY)
The only thing we can determine is if a mouse button is pressed and then calculate what direction the mouse is moving in left, right, up or down.
The idea is then to move the position of element with the image by setting the top and left CSS values as percentages.
This component code:
#using System.Text;
<div id="mainImageContainer" style="display: block;width:#($"{ImageWidthInPx}px");height:#($"{ImageHeightInPx}px");overflow: hidden;">
<div id="imageMover"
#onmousewheel="MouseWheelZooming"
style="#MoveImageStyle">
<div id="imageContainer"
#onmousemove="MouseMoving"
style="#ZoomImageStyle">
#*this div is used just for moving around when zoomed*#
</div>
</div>
</div>
#if (ShowResetButton)
{
<div style="display:block">
<button #onclick="ResetImgage">Reset</button>
</div>
}
#code{
/// <summary>
/// The path or url of the image
/// </summary>
[Parameter]
public string ImageUrlPath { get; set; }
/// <summary>
/// The width of the image
/// </summary>
[Parameter]
public int ImageWidthInPx { get; set; }
/// <summary>
/// The height of the image
/// </summary>
[Parameter]
public int ImageHeightInPx { get; set; }
/// <summary>
/// Set to true to show the reset button
/// </summary>
[Parameter]
public bool ShowResetButton { get; set; }
/// <summary>
/// Set the amount the image is scaled by, default is 0.1f
/// </summary>
[Parameter]
public double DefaultScaleBy { get; set; } = 0.1f;
/// <summary>
/// The Maximum the image can scale to, default = 5f
/// </summary>
[Parameter]
public double ScaleToMaximum { get; set; } = 5f;
/// <summary>
/// Set the speed at which the image is moved by, default 2.
/// 2 or 3 seems to work best.
/// </summary>
[Parameter]
public double DefaultMoveBy { get; set; } = 2;
//defaults
double _CurrentScale = 1.0f;
double _PositionLeft = 0;
double _PositionTop = 0;
double _OldClientX = 0;
double _OldClientY = 0;
double _DefaultMinPosition = 0;//to the top and left
double _DefaultMaxPosition = 0;//to the right and down
//the default settings used to display the image in the child div
private Dictionary<string, string> _ImageContainerStyles;
Dictionary<string, string> ImageContainerStyles
{
get
{
if (_ImageContainerStyles == null)
{
_ImageContainerStyles = new Dictionary<string, string>();
_ImageContainerStyles.Add("width", "100%");
_ImageContainerStyles.Add("height", "100%");
_ImageContainerStyles.Add("position", "relative");
_ImageContainerStyles.Add("background-size", "contain");
_ImageContainerStyles.Add("background-repeat", "no-repeat");
_ImageContainerStyles.Add("background-position", "50% 50%");
_ImageContainerStyles.Add("background-image", $"URL({ImageUrlPath})");
}
return _ImageContainerStyles;
}
}
private Dictionary<string, string> _MovingContainerStyles;
Dictionary<string, string> MovingContainerStyles
{
get
{
if (_MovingContainerStyles == null)
{
InvokeAsync(ResetImgage);
}
return _MovingContainerStyles;
}
}
protected async Task ResetImgage()
{
_PositionLeft = 0;
_PositionTop = 0;
_DefaultMinPosition = 0;
_DefaultMaxPosition = 0;
_CurrentScale = 1.0f;
_MovingContainerStyles = new Dictionary<string, string>();
_MovingContainerStyles.Add("width", "100%");
_MovingContainerStyles.Add("height", "100%");
_MovingContainerStyles.Add("position", "relative");
_MovingContainerStyles.Add("left", $"{_PositionLeft}%");
_MovingContainerStyles.TryAdd("top", $"{_PositionTop}%");
await InvokeAsync(StateHasChanged);
}
string ZoomImageStyle { get => DictionaryToCss(ImageContainerStyles); }
string MoveImageStyle { get => DictionaryToCss(MovingContainerStyles); }
private string DictionaryToCss(Dictionary<string, string> styleDictionary)
{
StringBuilder sb = new StringBuilder();
foreach (var kvp in styleDictionary.AsEnumerable())
{
sb.AppendFormat("{0}:{1};", kvp.Key, kvp.Value);
}
return sb.ToString();
}
protected async void MouseMoving(MouseEventArgs e)
{
//if the mouse button 1 is not down exit the function
if (e.Buttons != 1)
{
_OldClientX = e.ClientX;
_OldClientY = e.ClientY;
return;
}
//get the % of the current scale to move by at least the default move speed plus any scaled changes
//basically the bigger the image the faster it moves..
double scaleFrac = (_CurrentScale / ScaleToMaximum);
double scaleMove = (DefaultMoveBy * (DefaultMoveBy * scaleFrac));
//moving mouse right
if (_OldClientX < e.ClientX)
{
if ((_PositionLeft - DefaultMoveBy) <= _DefaultMaxPosition)
{
_PositionLeft += scaleMove;
}
}
//moving mouse left
if (_OldClientX > e.ClientX)
{
//if (_DefaultMinPosition < (_PositionLeft - DefaultMoveBy))
if ((_PositionLeft + DefaultMoveBy) >= _DefaultMinPosition)
{
_PositionLeft -= scaleMove;
}
}
//moving mouse down
if (_OldClientY < e.ClientY)
{
//if ((_PositionTop + DefaultMoveBy) <= _DefaultMaxPosition)
if ((_PositionTop - DefaultMoveBy) <= _DefaultMaxPosition)
{
_PositionTop += scaleMove;
}
}
//moving mouse up
if (_OldClientY > e.ClientY)
{
//if ((_PositionTop - DefaultMoveBy) > _DefaultMinPosition)
if ((_PositionTop + DefaultMoveBy) >= _DefaultMinPosition)
{
_PositionTop -= scaleMove;
}
}
_OldClientX = e.ClientX;
_OldClientY = e.ClientY;
await UpdateScaleAndPosition();
}
async Task<double> IncreaseScale()
{
return await Task.Run(() =>
{
//increase the scale first then calculate the max and min positions
_CurrentScale += DefaultScaleBy;
double scaleFrac = (_CurrentScale / ScaleToMaximum);
double scaleDiff = (DefaultMoveBy + (DefaultMoveBy * scaleFrac));
double scaleChange = DefaultMoveBy + scaleDiff;
_DefaultMaxPosition += scaleChange;
_DefaultMinPosition -= scaleChange;
return _CurrentScale;
});
}
async Task<double> DecreaseScale()
{
return await Task.Run(() =>
{
_CurrentScale -= DefaultScaleBy;
double scaleFrac = (_CurrentScale / ScaleToMaximum);
double scaleDiff = (DefaultMoveBy + (DefaultMoveBy * scaleFrac));
double scaleChange = DefaultMoveBy + scaleDiff;
_DefaultMaxPosition -= scaleChange;
_DefaultMinPosition += scaleChange;//DefaultMoveBy;
//fix descaling, move the image back into view when descaling (zoomin out)
if (_CurrentScale <= 1)
{
_PositionLeft = 0;
_PositionTop = 0;
}
else
{
//left can not be more than max position
_PositionLeft = (_DefaultMaxPosition < _PositionLeft) ? _DefaultMaxPosition : _PositionLeft;
//top can not be more than max position
_PositionTop = (_DefaultMaxPosition < _PositionTop) ? _DefaultMaxPosition : _PositionTop;
//left can not be less than min position
_PositionLeft = (_DefaultMinPosition > _PositionLeft) ? _DefaultMinPosition : _PositionLeft;
//top can not be less than min position
_PositionTop = (_DefaultMinPosition > _PositionTop) ? _DefaultMinPosition : _PositionTop;
}
return _CurrentScale;
});
}
protected async void MouseWheelZooming(WheelEventArgs e)
{
//holding shift stops the page from scrolling
if (e.ShiftKey == true)
{
if (e.DeltaY > 0)
{
_CurrentScale = ((_CurrentScale + DefaultScaleBy) >= 5) ? _CurrentScale = 5f : await IncreaseScale();
}
if (e.DeltaY < 0)
{
_CurrentScale = ((_CurrentScale - DefaultScaleBy) <= 0) ? _CurrentScale = DefaultScaleBy : await DecreaseScale();
}
await UpdateScaleAndPosition();
}
}
/// <summary>
/// Refresh the values in the moving style dictionary that is used to position the image.
/// </summary>
async Task UpdateScaleAndPosition()
{
await Task.Run(() =>
{
if (!MovingContainerStyles.TryAdd("transform", $"scale({_CurrentScale})"))
{
MovingContainerStyles["transform"] = $"scale({_CurrentScale})";
}
if (!MovingContainerStyles.TryAdd("left", $"{_PositionLeft}%"))
{
MovingContainerStyles["left"] = $"{_PositionLeft}%";
}
if (!MovingContainerStyles.TryAdd("top", $"{_PositionTop}%"))
{
MovingContainerStyles["top"] = $"{_PositionTop}%";
}
});
}
}
This is the usage:
#page "/"
#using BlazorWasmApp.Components
Welcome to your new app.
<ZoomableImageComponent ImageUrlPath="images/Capricorn.png"
ImageWidthInPx=400
ImageHeightInPx=300
ShowResetButton=true
DefaultScaleBy=0.1f />
and this is the result:
I only tested this in chrome on a desktop computer without touch input.

Zooming of an image using mousewheel.

In the code below I am trying to zoom the image via a mouse wheel. But the code is not working properly, it is just refreshing the panel but it does not resize it. Actually I am taking the image from memory stream that is created by another class called as decrypt. Complete Image is displayed properly but I am not able to performing zooming of the image using mousewheel event.
Plz help Me.
private void Form2_Load(object sender, EventArgs e)
{
this.Width = Screen.PrimaryScreen.WorkingArea.Width;
this.Height = Screen.PrimaryScreen.WorkingArea.Height;
this.CenterToScreen();
PicturePanel= new PictureBox();
PicturePanel.Dock = DockStyle.Fill;
//PicturePanel.SizeMode = PictureBoxSizeMode.AutoSize;
//PicturePanel.SizeMode = PictureBoxSizeMode.CenterImage;
PicturePanel.Focus();
//PicturePanel.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.OnMouseWheel);
this.Controls.Add(PicturePanel);
View_codee v = new View_codee();
try
{
PicturePanel.Image = Image.FromStream(Decrypt.ms1);
}
catch (Exception ee)
{
MessageBox.Show(ee.Message);
}
this.Name = "";
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
// Override OnMouseWheel event, for zooming in/out with the scroll wheel
if (PicturePanel.Image != null)
{
// If the mouse wheel is moved forward (Zoom in)
if (mea.Delta > 0)
{
// Check if the pictureBox dimensions are in range (15 is the minimum and maximum zoom level)
if ((PicturePanel.Width < (15 * this.Width)) && (PicturePanel.Height < (15 * this.Height)))
{
// Change the size of the picturebox, multiply it by the ZOOMFACTOR
PicturePanel.Width = (int)(PicturePanel.Width * 1.25);
PicturePanel.Height = (int)(PicturePanel.Height * 1.25);
// Formula to move the picturebox, to zoom in the point selected by the mouse cursor
PicturePanel.Top = (int)(mea.Y - 1.25 * (mea.Y - PicturePanel.Top));
PicturePanel.Left = (int)(mea.X - 1.25 * (mea.X - PicturePanel.Left));
}
}
else
{
// Check if the pictureBox dimensions are in range (15 is the minimum and maximum zoom level)
if ((PicturePanel.Width > (this.Width / 15)) && (PicturePanel.Height > (this.Height / 15)))
{
// Change the size of the picturebox, divide it by the ZOOMFACTOR
PicturePanel.Width = (int)(PicturePanel.Width / 1.25);
PicturePanel.Height = (int)(PicturePanel.Height / 1.25);
// Formula to move the picturebox, to zoom in the point selected by the mouse cursor
PicturePanel.Top = (int)(mea.Y - 0.80 * (mea.Y - PicturePanel.Top));
PicturePanel.Left = (int)(mea.X - 0.80 * (mea.X - PicturePanel.Left));
}
}
}
}
Source
Updated code by adding a new ImageProperty so you can set directly the Image;
public class PictureBox : System.Windows.Forms.UserControl
{
#region Members
private System.Windows.Forms.PictureBox PicBox;
private Panel OuterPanel;
private Container components = null;
private string m_sPicName = "";
#endregion
#region Constants
private double ZOOMFACTOR = 1.25; // = 25% smaller or larger
private int MINMAX = 5; // 5 times bigger or smaller than the ctrl
#endregion
#region Designer generated code
private void InitializeComponent()
{
this.PicBox = new System.Windows.Forms.PictureBox();
this.OuterPanel = new System.Windows.Forms.Panel();
this.OuterPanel.SuspendLayout();
this.SuspendLayout();
//
// PicBox
//
this.PicBox.Location = new System.Drawing.Point(0, 0);
this.PicBox.Name = "PicBox";
this.PicBox.Size = new System.Drawing.Size(150, 140);
this.PicBox.TabIndex = 3;
this.PicBox.TabStop = false;
//
// OuterPanel
//
this.OuterPanel.AutoScroll = true;
this.OuterPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.OuterPanel.Controls.Add(this.PicBox);
this.OuterPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this.OuterPanel.Location = new System.Drawing.Point(0, 0);
this.OuterPanel.Name = "OuterPanel";
this.OuterPanel.Size = new System.Drawing.Size(210, 190);
this.OuterPanel.TabIndex = 4;
//
// PictureBox
//
this.Controls.Add(this.OuterPanel);
this.Name = "PictureBox";
this.Size = new System.Drawing.Size(210, 190);
this.OuterPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
#region Constructors
public PictureBox()
{
InitializeComponent();
InitCtrl(); // my special settings for the ctrl
}
#endregion
#region Properties
private Image _pictureImage;
public Image PictureImage
{
get { return _pictureImage; }
set
{
if (null != value)
{
try
{
PicBox.Image = value;
_pictureImage = value;
}
catch (OutOfMemoryException ex)
{
RedCross();
}
}
else
{
RedCross();
}
}
}
/// <summary>
/// Property to select the picture which is displayed in the picturebox. If the
/// file doesn´t exist or we receive an exception, the picturebox displays
/// a red cross.
/// </summary>
/// <value>Complete filename of the picture, including path information</value>
/// <remarks>Supported fileformat: *.gif, *.tif, *.jpg, *.bmp</remarks>
///
[Browsable(false)]
public string Picture
{
get { return m_sPicName; }
set
{
if (null != value)
{
if (System.IO.File.Exists(value))
{
try
{
PicBox.Image = Image.FromFile(value);
m_sPicName = value;
}
catch (OutOfMemoryException ex)
{
RedCross();
}
}
else
{
RedCross();
}
}
}
}
/// <summary>
/// Set the frametype of the picturbox
/// </summary>
[Browsable(false)]
public BorderStyle Border
{
get { return OuterPanel.BorderStyle; }
set { OuterPanel.BorderStyle = value; }
}
#endregion
#region Other Methods
/// <summary>
/// Special settings for the picturebox ctrl
/// </summary>
private void InitCtrl()
{
PicBox.SizeMode = PictureBoxSizeMode.StretchImage;
PicBox.Location = new Point(0, 0);
OuterPanel.Dock = DockStyle.Fill;
OuterPanel.Cursor = System.Windows.Forms.Cursors.NoMove2D;
OuterPanel.AutoScroll = true;
OuterPanel.MouseEnter += new EventHandler(PicBox_MouseEnter);
PicBox.MouseEnter += new EventHandler(PicBox_MouseEnter);
OuterPanel.MouseWheel += new MouseEventHandler(PicBox_MouseWheel);
}
/// <summary>
/// Create a simple red cross as a bitmap and display it in the picturebox
/// </summary>
private void RedCross()
{
Bitmap bmp = new Bitmap(OuterPanel.Width, OuterPanel.Height, System.Drawing.Imaging.PixelFormat.Format16bppRgb555);
Graphics gr;
gr = Graphics.FromImage(bmp);
Pen pencil = new Pen(Color.Red, 5);
gr.DrawLine(pencil, 0, 0, OuterPanel.Width, OuterPanel.Height);
gr.DrawLine(pencil, 0, OuterPanel.Height, OuterPanel.Width, 0);
PicBox.Image = bmp;
gr.Dispose();
}
#endregion
#region Zooming Methods
/// <summary>
/// Make the PictureBox dimensions larger to effect the Zoom.
/// </summary>
/// <remarks>Maximum 5 times bigger</remarks>
private void ZoomIn()
{
if ((PicBox.Width < (MINMAX * OuterPanel.Width)) &&
(PicBox.Height < (MINMAX * OuterPanel.Height)))
{
PicBox.Width = Convert.ToInt32(PicBox.Width * ZOOMFACTOR);
PicBox.Height = Convert.ToInt32(PicBox.Height * ZOOMFACTOR);
PicBox.SizeMode = PictureBoxSizeMode.StretchImage;
}
}
/// <summary>
/// Make the PictureBox dimensions smaller to effect the Zoom.
/// </summary>
/// <remarks>Minimum 5 times smaller</remarks>
private void ZoomOut()
{
if ((PicBox.Width > (OuterPanel.Width / MINMAX)) &&
(PicBox.Height > (OuterPanel.Height / MINMAX)))
{
PicBox.SizeMode = PictureBoxSizeMode.StretchImage;
PicBox.Width = Convert.ToInt32(PicBox.Width / ZOOMFACTOR);
PicBox.Height = Convert.ToInt32(PicBox.Height / ZOOMFACTOR);
}
}
#endregion
#region Mouse events
/// <summary>
/// We use the mousewheel to zoom the picture in or out
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PicBox_MouseWheel(object sender, MouseEventArgs e)
{
if (e.Delta < 0)
{
ZoomIn();
}
else
{
ZoomOut();
}
}
/// <summary>
/// Make sure that the PicBox have the focus, otherwise it doesn´t receive
/// mousewheel events !.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PicBox_MouseEnter(object sender, EventArgs e)
{
if (PicBox.Focused == false)
{
PicBox.Focus();
}
}
#endregion
#region Disposing
/// <summary>
/// Die verwendeten Ressourcen bereinigen.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
components.Dispose();
}
base.Dispose(disposing);
}
#endregion
}
private void Form2_Load(object sender, EventArgs e)
{
pictureBox1.PictureImage = Image.FromStream(Decrypt.ms1);
}

Handle scrolling of a WinForms control manually

I have a control (System.Windows.Forms.ScrollableControl) which can potentially be very large. It has custom OnPaint logic. For that reason, I am using the workaround described here.
public class CustomControl : ScrollableControl
{
public CustomControl()
{
this.AutoScrollMinSize = new Size(100000, 500);
this.DoubleBuffered = true;
}
protected override void OnScroll(ScrollEventArgs se)
{
base.OnScroll(se);
this.Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var graphics = e.Graphics;
graphics.Clear(this.BackColor);
...
}
}
The painting code mainly draws "normal" things that move when you scroll. The origin of each shape that is drawn is offsetted by this.AutoScrollPosition.
graphics.DrawRectangle(pen, 100 + this.AutoScrollPosition.X, ...);
However, the control also contains "static" elements, which are always drawn at the same position relative to the parent control. For that, I just don't use AutoScrollPosition and draw the shapes directly:
graphics.DrawRectangle(pen, 100, ...);
When the user scrolls, Windows translates the entire visible area in the direction opposite to the scrolling. Usually this makes sense, because then the scrolling seems smooth and responsive (and only the new part has to be redrawn), however the static parts are also affected by this translation (hence the this.Invalidate() in OnScroll). Until the next OnPaint call has successfully redrawn the surface, the static parts are slightly off. This causes a very noticable "shaking" effect when scrolling.
Is there a way I can create a scrollable custom control that does not have this problem with static parts?
You could do this by taking full control of scrolling. At the moment, you're just hooking in to the event to do your logic. I've faced issues with scrolling before, and the only way I've ever managed to get everything to work smoothly is by actually handling the Windows messages by overriding WndProc. For instance, I have this code to synchronize scrolling between several ListBoxes:
protected override void WndProc(ref Message m) {
base.WndProc(ref m);
// 0x115 and 0x20a both tell the control to scroll. If either one comes
// through, you can handle the scrolling before any repaints take place
if (m.Msg == 0x115 || m.Msg == 0x20a)
{
//Do you scroll processing
}
}
Using WndProc will get you the scroll messages before anything gets repainted at all, so you can appropriately handle the static objects. I'd use this to suspend scrolling until an OnPaint occurs. It won't look as smooth, but you won't have issues with the static objects moving.
Since I really needed this, I ended up writing a Control specifically for the case when you have static graphics on a scrollable surface (whose size can be greater than 65535).
It is a regular Control with two ScrollBar controls on it, and a user-assignable Control as its Content. When the user scrolls, the container sets its Content's AutoScrollOffset accordingly. Therefore, it is possible to use controls which use the AutoScrollOffset method for drawing without changing anything. The Content's actual size is exactly the visible part of it at all times. It allows horizontal scrolling by holding down the shift key.
Usage:
var container = new ManuallyScrollableContainer();
var content = new ExampleContent();
container.Content = content;
container.TotalContentWidth = 150000;
container.TotalContentHeight = 5000;
container.Dock = DockStyle.Fill;
this.Controls.Add(container); // e.g. add to Form
Code:
It became a bit lengthy, but I could avoid ugly hacks. Should work with mono. I think it turned out pretty sane.
public class ManuallyScrollableContainer : Control
{
public ManuallyScrollableContainer()
{
InitializeControls();
}
private class UpdatingHScrollBar : HScrollBar
{
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
// setting the scroll position programmatically shall raise Scroll
this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
}
}
private class UpdatingVScrollBar : VScrollBar
{
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
// setting the scroll position programmatically shall raise Scroll
this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
}
}
private ScrollBar shScrollBar;
private ScrollBar svScrollBar;
public ScrollBar HScrollBar
{
get { return this.shScrollBar; }
}
public ScrollBar VScrollBar
{
get { return this.svScrollBar; }
}
private void InitializeControls()
{
this.Width = 300;
this.Height = 300;
this.shScrollBar = new UpdatingHScrollBar();
this.shScrollBar.Top = this.Height - this.shScrollBar.Height;
this.shScrollBar.Left = 0;
this.shScrollBar.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
this.svScrollBar = new UpdatingVScrollBar();
this.svScrollBar.Top = 0;
this.svScrollBar.Left = this.Width - this.svScrollBar.Width;
this.svScrollBar.Anchor = AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;
this.shScrollBar.Width = this.Width - this.svScrollBar.Width;
this.svScrollBar.Height = this.Height - this.shScrollBar.Height;
this.Controls.Add(this.shScrollBar);
this.Controls.Add(this.svScrollBar);
this.shScrollBar.Scroll += this.HandleScrollBarScroll;
this.svScrollBar.Scroll += this.HandleScrollBarScroll;
}
private Control _content;
/// <summary>
/// Specifies the control that should be displayed in this container.
/// </summary>
public Control Content
{
get { return this._content; }
set
{
if (_content != value)
{
RemoveContent();
this._content = value;
AddContent();
}
}
}
private void AddContent()
{
if (this.Content != null)
{
this.Content.Left = 0;
this.Content.Top = 0;
this.Content.Width = this.Width - this.svScrollBar.Width;
this.Content.Height = this.Height - this.shScrollBar.Height;
this.Content.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
this.Controls.Add(this.Content);
CalculateMinMax();
}
}
private void RemoveContent()
{
if (this.Content != null)
{
this.Controls.Remove(this.Content);
}
}
protected override void OnParentChanged(EventArgs e)
{
// mouse wheel events only arrive at the parent control
if (this.Parent != null)
{
this.Parent.MouseWheel -= this.HandleMouseWheel;
}
base.OnParentChanged(e);
if (this.Parent != null)
{
this.Parent.MouseWheel += this.HandleMouseWheel;
}
}
private void HandleMouseWheel(object sender, MouseEventArgs e)
{
this.HandleMouseWheel(e);
}
/// <summary>
/// Specifies how the control reacts to mouse wheel events.
/// Can be overridden to adjust the scroll speed with the mouse wheel.
/// </summary>
protected virtual void HandleMouseWheel(MouseEventArgs e)
{
// The scroll difference is calculated so that with the default system setting
// of 3 lines per scroll incremenet,
// one scroll will offset the scroll bar value by LargeChange / 4
// i.e. a quarter of the thumb size
ScrollBar scrollBar;
if ((Control.ModifierKeys & Keys.Shift) != 0)
{
scrollBar = this.HScrollBar;
}
else
{
scrollBar = this.VScrollBar;
}
var minimum = 0;
var maximum = scrollBar.Maximum - scrollBar.LargeChange;
if (maximum <= 0)
{
// happens when the entire area is visible
return;
}
var value = scrollBar.Value - (int)(e.Delta * scrollBar.LargeChange / (120.0 * 12.0 / SystemInformation.MouseWheelScrollLines));
scrollBar.Value = Math.Min(Math.Max(value, minimum), maximum);
}
public event ScrollEventHandler Scroll;
protected virtual void OnScroll(ScrollEventArgs e)
{
var handler = this.Scroll;
if (handler != null)
{
handler(this, e);
}
}
/// <summary>
/// Event handler for the Scroll event of either scroll bar.
/// </summary>
private void HandleScrollBarScroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
if (this.Content != null)
{
this.Content.AutoScrollOffset = new System.Drawing.Point(-this.HScrollBar.Value, -this.VScrollBar.Value);
this.Content.Invalidate();
}
}
private int _totalContentWidth;
public int TotalContentWidth
{
get { return _totalContentWidth; }
set
{
if (_totalContentWidth != value)
{
_totalContentWidth = value;
CalculateMinMax();
}
}
}
private int _totalContentHeight;
public int TotalContentHeight
{
get { return _totalContentHeight; }
set
{
if (_totalContentHeight != value)
{
_totalContentHeight = value;
CalculateMinMax();
}
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
CalculateMinMax();
}
private void CalculateMinMax()
{
if (this.Content != null)
{
// Reduced formula according to
// http://msdn.microsoft.com/en-us/library/system.windows.forms.scrollbar.maximum.aspx
// Note: The original formula is bogus.
// According to the article, LargeChange has to be known in order to calculate Maximum,
// however, that is not always possible because LargeChange cannot exceed Maximum.
// If (LargeChange) == (1 * visible part of control), the formula can be reduced to:
if (this.TotalContentWidth > this.Content.Width)
{
this.shScrollBar.Enabled = true;
this.shScrollBar.Maximum = this.TotalContentWidth;
}
else
{
this.shScrollBar.Enabled = false;
}
if (this.TotalContentHeight > this.Content.Height)
{
this.svScrollBar.Enabled = true;
this.svScrollBar.Maximum = this.TotalContentHeight;
}
else
{
this.svScrollBar.Enabled = false;
}
// this must be set after the maximum is determined
this.shScrollBar.LargeChange = this.shScrollBar.Width;
this.shScrollBar.SmallChange = this.shScrollBar.LargeChange / 10;
this.svScrollBar.LargeChange = this.svScrollBar.Height;
this.svScrollBar.SmallChange = this.svScrollBar.LargeChange / 10;
}
}
}
Example content:
public class ExampleContent : Control
{
public ExampleContent()
{
this.DoubleBuffered = true;
}
static Random random = new Random();
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var graphics = e.Graphics;
// random color to make the clip rectangle visible in an unobtrusive way
var color = Color.FromArgb(random.Next(160, 180), random.Next(160, 180), random.Next(160, 180));
graphics.Clear(color);
Debug.WriteLine(this.AutoScrollOffset.X.ToString() + ", " + this.AutoScrollOffset.Y.ToString());
CheckerboardRenderer.DrawCheckerboard(
graphics,
this.AutoScrollOffset,
e.ClipRectangle,
new Size(50, 50)
);
StaticBoxRenderer.DrawBoxes(graphics, new Point(0, this.AutoScrollOffset.Y), 100, 30);
}
}
public static class CheckerboardRenderer
{
public static void DrawCheckerboard(Graphics g, Point origin, Rectangle bounds, Size squareSize)
{
var numSquaresH = (bounds.Width + squareSize.Width - 1) / squareSize.Width + 1;
var numSquaresV = (bounds.Height + squareSize.Height - 1) / squareSize.Height + 1;
var startBoxH = (bounds.X - origin.X) / squareSize.Width;
var startBoxV = (bounds.Y - origin.Y) / squareSize.Height;
for (int i = startBoxH; i < startBoxH + numSquaresH; i++)
{
for (int j = startBoxV; j < startBoxV + numSquaresV; j++)
{
if ((i + j) % 2 == 0)
{
Random random = new Random(i * j);
var color = Color.FromArgb(random.Next(70, 95), random.Next(70, 95), random.Next(70, 95));
var brush = new SolidBrush(color);
g.FillRectangle(brush, i * squareSize.Width + origin.X, j * squareSize.Height + origin.Y, squareSize.Width, squareSize.Height);
brush.Dispose();
}
}
}
}
}
public static class StaticBoxRenderer
{
public static void DrawBoxes(Graphics g, Point origin, int boxWidth, int boxHeight)
{
int height = origin.Y;
int left = origin.X;
for (int i = 0; i < 25; i++)
{
Rectangle r = new Rectangle(left, height, boxWidth, boxHeight);
g.FillRectangle(Brushes.White, r);
g.DrawRectangle(Pens.Black, r);
height += boxHeight;
}
}
}

Drawing list of pieces on the form

First of all I'm new to the C# world.
I just started this year learning about C# with no programming background experience.
I came here with a problem that I would like to get solved asap.
I managed to write a little code to create a list with Bitmaps including the draw positions.
What I'm trying to do is to draw every piece (picture of a dot) in the list on the form.
I've been working hours on this just trying to figure out how to get the list drawn on the form...
I placed comments behind the code to make it easier for the reader to understand whats it's for so it won't cause any brain damage or sweat to the reader. :p
See below for my code:
Form1.cs
public partial class Form1 : Form
{
private GridDrawing drawing;
private Bitmap bmpPic;
public Form1()
{
InitializeComponent();
bmpPic = new Bitmap("Dot.png"); // Picture(Dot 28*28 pixels)
this.Paint += Form_Paint;
}
private void Form_Paint(object sender, PaintEventArgs e)
{
drawing = new GridDrawing(this, bmpPic, 6, 8); // Form, Bitmap, Rows, Columns
foreach (var piece in drawing.Pieces)
{
e.Graphics.DrawImage(bmpPic, piece.Position);
}
}
private void btnStart_Click(object sender, PaintEventArgs e)
{
}
}
GridDrawing.cs
public class GridDrawing
{
private Bitmap bmpPic;
private int columns;
private int rows;
private List<GridPiece> pieces;
private Point position;
/// <summary>
/// Constructs a grid with dots.
/// </summary>
/// <param name="ctrl"></param>
/// <param name="gridPic"></param>
/// <param name="rows"></param>
/// <param name="columns"></param>
public GridDrawing(Control ctrl, Bitmap bmpPic, int rows, int columns)
{
this.bmpPic = bmpPic; //The picture(Dot).
this.rows = rows; //The amount of rows in the matrix.
this.columns = columns; //The amount of columns in the matrix.
this.pieces = new List<GridPiece>(); //Initializes the List GridPieces
Point position = new Point(0, 0); //Draw position of the picture(Dot)
Size size = new Size(bmpPic.Width, bmpPic.Height); //Size of picture(Dot).
for (int i = 0; i <= rows; i++) //A with 6 rows
{
position.X = 0; //Puts the value X on 0 when it starts a new row.
for (int j = 0; j <= columns; j++) //A matrix with 8 columns
{
GridPiece s = new GridPiece(bmpPic, position); // Creates a piece
pieces.Add(s); // Puts the piece that has to be drawn in the List<GridPiece>pieces
position.X += size.Width; // Changes the width of the draw position
}
position.Y += size.Height; // Changes the height of the draw position
}
}
public List<GridPiece> Pieces
{
get { return this.pieces; }
}
}
GridPiece.cs
public class GridPiece
{
private Bitmap bmpPic;
private Point position;
/// <summary>
/// Constructor of GriedPiece
/// </summary>
/// <param name="bmpPic"></param>
/// <param name="position"></param>
public GridPiece(Bitmap bmpPic, Point position)
{
this.bmpPic = bmpPic;
this.position = position;
}
public Point Position
{
get { return position; }
}
}
Could anyone pretty please help me solve my issue?
I updates the code several times.
You need to handle the form's Paint event, and use the methods in e.Graphics to draw your pieces in a loop.
(Probably e.Graphics.DrawImage, or perhaps FillCircle)
It would look something like
void Form_Paint(object sender, PaintEventArgs e) {
foreach(var piece in drawing.Pieces) {
e.Graphics.DrawImage(bmpPic, piece.Position);
}
}
The paint event is raised every time the forms needs to be drawn.
When the pieces move, you need to manually force the form to repaint by calling Invalidate().

Categories