I have the following class:
class Sapo
{
private Image imgSapo;
public int IdSapo { get; }
public Sapo(int id)
{
imgSapo = new Image();
IdSapo = id;
}
public Image show
{
get
{
imgSapo.Source = new BitmapImage(new Uri("pack://application:,,,/Imagens/sapo.png", UriKind.Absolute));
imgSapo.Width = 56;
imgSapo.Height = 56;
return imgSapo;
}
}
}
And I have a method where I create a thread passing an instance of each object of class Sapo:
public void CriarSapos()
{
Thread th;
int sapos = int.Parse(txt_sapos.Text);
Sapo[] arraySapos = new Sapo[sapos];
for (int i = 0; i < sapos; i++)
{
th = new Thread(new ParameterizedThreadStart(ThreadImageSapo));
th.SetApartmentState(ApartmentState.STA);
th.IsBackground = true;
arraySapos[i] = new Sapo(th.ManagedThreadId);
th.Start(arraySapos[i]);
}
}
The method responsible for inserting the images on the canvas:
public void ThreadImageSapo(object obj)
{
Dispatcher.Invoke(() =>
{
Sapo _sapo = (Sapo)obj;
double max_x = _canvas.ActualWidth - _sapo.show.Width;
double max_y = _canvas.ActualHeight - _sapo.show.Height;
Canvas.SetLeft(_sapo.show, rnd.NextDouble() * max_x);
Canvas.SetTop(_sapo.show, rnd.NextDouble() * max_y);
_canvas.Children.Add(_sapo.show);
});
}
My objective is that I want to check the collision of images in the canvas and destroy the thread in which the object of that image consists
My question is: How can I get the respective object of the canvas image?
First of all, you should generally not create UI elements in code behind, especially in a non-UI thread. Change you Sapo class so that it hold a BitmapImage in a property, instead of an Image control. Also make sure to call Freeze() on the BitmapImage to make it cross-thread accessible.
public class Sapo
{
public int IdSapo { get; private set; }
public double Width { get; private set; }
public double Height { get; private set; }
public BitmapImage Image { get; private set; }
public Sapo(int id)
{
IdSapo = id;
Width = 56;
Height = 56;
Image = new BitmapImage(new Uri("pack://application:,,,/Imagens/sapo.png"));
Image.Freeze();
}
}
Now when you add the image to a Canvas, create an Image control and assign the BitmapImage like this:
Dispatcher.Invoke(() =>
{
var sapo = (Sapo)obj;
var image = new Image
{
Source = sapo.Image,
Width = sapo.Width,
Height = sapo.Height
};
double maxX = _canvas.ActualWidth - image.Width;
double maxY = _canvas.ActualHeight - image.Height;
Canvas.SetLeft(image, rnd.NextDouble() * maxX);
Canvas.SetTop(image, rnd.NextDouble() * maxY);
_canvas.Children.Add(image);
});
This still creates an Image control in code behind, but in the UI thread. A better solution might be to use an ItemsControl with a Canvas as ItemsPanel.
Related
I have implemented a custom layout whose main characteristic is that the background can be a gradient by giving 2 hex colours. The view itself works perfectly fine, the problem is that changes made in these 2 colours while the program is running do not reflect themselves in the app, the custom Layout receives and changes both new colours and make the gradient correctly, but it doesn't show the changes made. I assume that the problem is that I have not implemented correctly the I INotifyPropertyChanged.
Here is the code of my custom view:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
namespace MyProject.Renderers
{
public class GradientLayout : StackLayout, INotifyPropertyChanged
{
public string ColorsList { get; set; }
public Color[] Colors
{
get
{
//colorsList have the following format: "HexCode1,HexCode2"
string[] hex = ColorsList.Split(',');
Color[] colors = new Color[hex.Length];
for (int i = 0; i < hex.Length; i++)
{
colors[i] = Color.FromHex(hex[i].Trim());
}
return colors;
}
}
//since this is a property that I am not trying to modify, I will not provide the rest of its code to make this as simple as possible.
public GradientColorStackMode Mode { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And that is all, if you need more information I will provide it as soon as I see your request, thank to all of you for your time, I hope you have a good day.
EDIT: Here you have a class in myProject.ios that is related to the background gradient.
[assembly: ExportRenderer(typeof(GradientLayout), typeof(GradientLayoutRenderer))]
namespace MyProject.iOS.Renderers
{
public class GradientLayoutRenderer : VisualElementRenderer<StackLayout>
{
public override void Draw(CGRect rect)
{
base.Draw(rect);
GradientLayout layout = (GradientLayout)Element;
CGColor[] colors = new CGColor[layout.Colors.Length];
for (int i = 0, l = colors.Length; i < l; i++)
{
colors[i] = layout.Colors[i].ToCGColor();
}
var gradientLayer = new CAGradientLayer();
switch (layout.Mode)
{
default:
case GradientColorStackMode.ToRight:
gradientLayer.StartPoint = new CGPoint(0, 0.5);
gradientLayer.EndPoint = new CGPoint(1, 0.5);
break;
case GradientColorStackMode.ToLeft:
gradientLayer.StartPoint = new CGPoint(1, 0.5);
gradientLayer.EndPoint = new CGPoint(0, 0.5);
break;
case GradientColorStackMode.ToTop:
gradientLayer.StartPoint = new CGPoint(0.5, 0);
gradientLayer.EndPoint = new CGPoint(0.5, 1);
break;
case GradientColorStackMode.ToBottom:
gradientLayer.StartPoint = new CGPoint(0.5, 1);
gradientLayer.EndPoint = new CGPoint(0.5, 0);
break;
case GradientColorStackMode.ToTopLeft:
gradientLayer.StartPoint = new CGPoint(1, 0);
gradientLayer.EndPoint = new CGPoint(0, 1);
break;
case GradientColorStackMode.ToTopRight:
gradientLayer.StartPoint = new CGPoint(0, 1);
gradientLayer.EndPoint = new CGPoint(1, 0);
break;
case GradientColorStackMode.ToBottomLeft:
gradientLayer.StartPoint = new CGPoint(1, 1);
gradientLayer.EndPoint = new CGPoint(0, 0);
break;
case GradientColorStackMode.ToBottomRight:
gradientLayer.StartPoint = new CGPoint(0, 0);
gradientLayer.EndPoint = new CGPoint(1, 1);
break;
}
gradientLayer.Frame = rect;
gradientLayer.Colors = colors;
NativeView.Layer.InsertSublayer(gradientLayer, 0);
}
}
}
EDIT: Also here is the xaml and xaml.cs code related to the declaration and calling of the gradient layout.
XAML.CS:
page.ColorsList = Items[0].StartColor+","+Items[0].EndColor;
page.Mode = MyProject.Renderers.GradientColorStackMode.ToBottomLeft;
XAML:
<renderers:GradientLayout
x:Name="page"
Opacity="0"
Mode="ToBottomLeft">
</renders:GradientLayout>
EDIT: I tried to implement Junior JiangĀ“s, after I did it I noticed 1 change, the IOS class was executing before the constructor of my xaml.cs class, giving me in string[] hex = ColorsList.Split(','); the same error as I previously mentioned because ColorsList had not received any value, so I gave 2 default values to "colors" and implemented a conditional to make sure the constructor code was not executing with ColorsList being null:
public GradientLayout()
{
if(colorsList != null)
{
//colorsList have the following format: "HexCode1,HexCode2"
string[] hex = ColorsList.Split(',');
colors = new Color[hex.Length];
for (int i = 0; i < hex.Length; i++)
{
colors[i] = Color.FromHex(hex[i].Trim());
}
// BindingContext = this;
}
else
{
colors = new Color[2];
colors[0] = Color.FromHex("#000000");
colors[1] = Color.FromHex("#000000");
}
}
After that executing the code did not give any errors, but it was not working as expected since the IOS class only calls the class 1 time (when constructing the view).
I also did not bind anything because I don't understand what I am supposed to do in //BindingContext = this
I think you should implement the INotifyPropertyChanged function something like as follow :
public class GradientLayout : StackLayout, INotifyPropertyChanged
{
private Color[] colors;
public string ColorsList { get; set; }
public Color[] Colors
{
set
{
if (colors != value)
{
colors = value;
OnPropertyChanged("Colors");
}
}
get
{
return colors;
}
}
public GradientLayout()
{
//colorsList have the following format: "HexCode1,HexCode2"
string[] hex = ColorsList.Split(',');
colors = new Color[hex.Length];
for (int i = 0; i < hex.Length; i++)
{
colors[i] = Color.FromHex(hex[i].Trim());
}
// BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And better using Model to bind properties for Control .Having a look at this doc .
==================================Update===================================
You can use Bindable Property to implement that .
Creat a CustomStackLayout :
public class CustomStackLayout : StackLayout
{
public static readonly BindableProperty ColorsProperty = BindableProperty.Create("Colors", typeof(string), typeof(CustomStackLayout), "Default Value");
public string Colors
{
get { return (string)GetValue(ColorsProperty); }
set { SetValue(ColorsProperty, value); }
}
}
In Xaml , adding a Tap method to change property of CustomStackLayout :
<local:CustomStackLayout HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
BackgroundColor="Gray">
<local:CustomStackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</local:CustomStackLayout.GestureRecognizers>
</local:CustomStackLayout>
The TapGestureRecognizer_Tapped method :
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
Console.WriteLine("--TapGestureRecognizer_Tapped--");
var customStacklayout = sender as CustomStackLayout;
customStacklayout.Colors = "new value";
}
Therefore in iOS Renderer file , there is a OnElementPropertyChanged method can listen which propery changed . Here you also can update UI .
[assembly: ExportRenderer(typeof(CustomStackLayout),typeof(CustomStackLayoutRenderer))]
namespace AppFrameRenderer.iOS
{
public class CustomStackLayoutRenderer : ViewRenderer
{
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var customStack = sender as CustomStackLayout;
Console.WriteLine("----" + customStack.Colors);
if (customStack.Colors == "Default Value")
{
//--
}
else
{
BackgroundColor = UIColor.Red;
}
}
}
}
The effect as follow :
I just wrote thsi code to have an access to images
private Bitmap[] hi = { HangmanUrdu.Properties.Resources._4, HangmanUrdu.Properties.Resources._5, HangmanUrdu.Properties.Resources._6, HangmanUrdu.Properties.Resources._7, HangmanUrdu.Properties.Resources._8, HangmanUrdu.Properties.Resources._9, HangmanUrdu.Properties.Resources._10 };
but when i want to increment the index and get these images in my picture box
// wg is just a counter;
pictureBox1.Image = hi { wg}; i
t throws me an error saying
cannot implicitly convert Bitmap to images
I also tried to change my array from bitmap to image but then it shows me error that cannot convert Images to Images.
Create a List<Bitmap> - as a Field, here - or any other type that fits the design (a class property, for example).
Fill the List<Bitmap> in a Form's constructor with the Bitmap objects needed in that context, creating a new Bitmap from the resource object:
private List<Bitmap> hi = null;
public Form1()
{
InitializeComponent();
this.hi = new List<Bitmap>()
{
new Bitmap(Properties.Resources._4),
new Bitmap(Properties.Resources._5)
};
}
The assign a Bitmap to a control's Image property when you need to:
pictureBox1.Image = hi[1];
You could also build a specialized class that hold these references, so you can access them with different naming conventions.
For example:
private List<BitmapResource> BitmapResources = null;
public Form1()
{
InitializeComponent();
this.BitmapResources = new List<BitmapResource>()
{
new BitmapResource(new Bitmap(Properties.Resources._4), "Logo"),
new BitmapResource(new Bitmap(Properties.Resources._5), "Watermark")
};
}
internal class BitmapResource
{
public BitmapResource(Bitmap bitmap, string imageName)
{
this.Image = bitmap;
this.Name = imageName;
}
public Bitmap Image { get; private set; }
public string Name { get; private set; }
}
Then, when needed:
By index:
pictureBox1.Image = BitmapResources[0].Image;
By name (simplified):
pictureBox1.Image = BitmapResources.FirstOrDefault(res => res.Name == "Logo").Image;
I have a paged UIScrollView that's populated with a collection of UIImageViews. The scroll view shows a preview of the previous and next page.
My goal is to make each page of the scroll view zoomable. Here's a gif of the problem I'm experiencing, where the other pages appear to change their coordinates as the image is zoomed:
I believe the zoom itself is causing the ContentSize of the scroll view to change drastically, which throws everything else out of sorts.
StoryBoard Hierarchy
[Controller]
---- [UIView]
-------- [UIScrollView], Custom Class CapturedResultScrollView
My UIScrollView has the following constraints to its parent UIView:
Code
Here's the CapturedResultScrollView implementation, as well as the delegate:
public class CaptureResultScrollViewDelegate : UIScrollViewDelegate {
public CaptureResultScrollViewDelegate(IHandlePaging pageHandler) {
this.pageHandler = pageHandler ?? throw new ArgumentNullException(nameof(pageHandler));
}
private readonly IHandlePaging pageHandler;
public override UIView ViewForZoomingInScrollView(UIScrollView scrollView) {
var pageIndex = (int)Math.Round(scrollView.ContentOffset.X / scrollView.Frame.Size.Width);
return pageHandler.Pages[pageIndex];
}
}
public interface IHandlePaging {
UIImageView[] Pages { get; set; }
}
public partial class CaptureResultScrollView : UIScrollView, IHandlePaging {
public CaptureResultScrollView(IntPtr handle) : base(handle) {
MinimumZoomScale = 1.0f;
MaximumZoomScale = 3.0f;
ZoomScale = 1.0f;
PagingEnabled = true;
ClipsToBounds = false;
ShowsHorizontalScrollIndicator = false;
ShowsVerticalScrollIndicator = false;
Delegate = new CaptureResultScrollViewDelegate(this);
}
public UIImageView[] Pages { get; set; }
public void Setup(IList<CapturedResultModel> capturedResults) {
// setup called from another controller during segue
Pages = new UIImageView[capturedResults.Count];
var pageSize = Frame.Size;
ContentSize = new CGSize(pageSize.Width * Pages.Length, pageSize.Height);
for (int page = 0; page < capturedResults.Count; page++) {
var imageView = new UIImageView() {
// hardcoded value allow for preview of previous/next pages.
Frame = new CGRect(pageSize.Width * page, 0, pageSize.Width - 30, pageSize.Height),
ContentMode = UIViewContentMode.ScaleAspectFill,
Image = UIImage.FromFile(capturedResults[page].ResultImagePath),
ClipsToBounds = true
};
AddSubview(imageView);
Pages[page] = imageView;
}
}
}
The Setup method of CaptureResultScrollView is called when the containing controller's ViewWillAppear event is triggered as follows:
public override void ViewWillAppear(bool animated) {
base.ViewWillAppear(animated);
CapturedResultScrollView.Setup(CapturedResults);
}
What am I doing wrong here?
Cause:
I think your problem is that you set your imageView for the method ViewForZoomingInScrollView, when you zoom the imageview you selected, only that imageView will zoom and the other imageViews will keep their positions in their superView(here is ScrollerView),so the other pages appear to change their coordinates as the image is zoomed.
public override UIView ViewForZoomingInScrollView(UIScrollView scrollView) {
var pageIndex = (int)Math.Round(scrollView.ContentOffset.X / scrollView.Frame.Size.Width);
return pageHandler.Pages[pageIndex];
}
Solution:
My advice is that you can add a view who has the same frame with the ScrollView as your imageView's superview. And set this view to the method ViewForZoomingInScrollView's return value instead of the imageView,so when you zoom this view, imageViews will zoom correctly.
Here is my code:
1.Add a backView in you IHandlePaging interface
public interface IHandlePaging
{
UIImageView[] Pages { get; set; }
UIView backView { get; set; }
}
2.Add this backView to the ScrovllView and Add the ImageViews to this backView
public void Setup(IList<CapturedResultModel> capturedResults)
{
Pages = new UIImageView[capturedResults.Count];
var pageSize = Frame.Size;
ContentSize = new CGSize(pageSize.Width * Pages.Length, pageSize.Height);
backView = new UIView()
{
//set the backView's size same as scrollview's contentSize
Frame = new CGRect(15,40,pageSize.Width * capturedResults.Count, pageSize.Height),
BackgroundColor = UIColor.Blue,
ClipsToBounds = true
};
Add(backView);
for (int page = 0; page < capturedResults.Count; page++)
{
var imageView = new UIImageView()
{
Frame = new CGRect( pageSize.Width * page, 0,pageSize.Width - 30, pageSize.Height),
Image = UIImage.FromBundle(capturedResults[page].ResultImagePath),
BackgroundColor = UIColor.Blue,
ClipsToBounds = true
};
backView.AddSubview(imageView);
Pages[page] = imageView;
}
}
3.Set the backView to the method ViewForZoomingInScrollView's return Value
public class CaptureResultScrollViewDelegate : UIScrollViewDelegate
{
public CaptureResultScrollViewDelegate(IHandlePaging pageHandler)
{
this.pageHandler = pageHandler ?? throw new ArgumentNullException(nameof(pageHandler));
}
private readonly IHandlePaging pageHandler;
public override UIView ViewForZoomingInScrollView(UIScrollView scrollView)
{
var pageIndex = (int)Math.Round(scrollView.ContentOffset.X / scrollView.Frame.Size.Width);
return pageHandler.backView;
}
}
4.At last, call the Setup method of CaptureResultScrollView
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
IList<CapturedResultModel> CapturedResults = new List< CapturedResultModel >{ new CapturedResultModel { ResultImagePath = "Images1" },
new CapturedResultModel { ResultImagePath = "Images2"},
new CapturedResultModel { ResultImagePath = "Images1" }
};
CaptureResultScrollView CapturedResultScrollView = new CaptureResultScrollView();
CapturedResultScrollView.Frame = new CGRect(15, 40, View.Frame.Size.Width - 30, View.Frame.Size.Height-80);
CapturedResultScrollView.Setup(CapturedResults);
Add(CapturedResultScrollView);
}
I have a windowsservice that is supposed to generate thumbprints of videos.
To achieve this i am trying to use the MediaElement-class (https://msdn.microsoft.com/en-us/library/system.windows.controls.mediaelement(v=vs.110).aspx). To fire events when opening medias this element has to be in a visualtree. Is it possible to simulate or fake a visual tree ?
Edit: I now use MediaPlayer instead of MediaElements but can't rely on the events because they are not thrown.
Updated code
public class Mp4ThumbnailExtractor : IDisposable
{
private ManualResetEvent waitHandle;
private TimeSpan mediaOpenTimeout;
public TimeSpan Time { get; set; }
= TimeSpan.Parse("00:00:30");
public TimeSpan FallbackTime { get; set; }
= TimeSpan.Parse("00:00:10");
public int Height { get; set; }
= 400;
public int Width { get; set; }
= 400;
public int Dpi { get; set; }
= 96;
public Mp4ThumbnailExtractor()
{
waitHandle = new ManualResetEvent(false);
mediaOpenTimeout = TimeSpan.Parse("00:00:15");
}
public MemoryStream CreateThumbnail(string videoPath)
{
MemoryStream memory = new MemoryStream();
Uri sourceUri = new Uri(videoPath);
MediaPlayer media = new MediaPlayer();
media.MediaOpened += Media_MediaOpened;
media.ScrubbingEnabled = true;
try
{
media.Open(new Uri(videoPath));
//TODO: media.opened event will not get fired
Thread.Sleep(mediaOpenTimeout);
waitHandle.Set();
//15sec timeout for loading the media
if (waitHandle.WaitOne(mediaOpenTimeout))
{
SetPosition(media);
//TODO: this is bad...
Thread.Sleep(2000);
DrawingVisual dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();
dc.DrawVideo(media, new Rect(0, 0, Width, Height));
dc.Close();
RenderTargetBitmap bmp = new RenderTargetBitmap(Width, Height, Dpi, Dpi, PixelFormats.Pbgra32);
bmp.Render(dv);
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
encoder.Save(memory);
memory.Seek(0, SeekOrigin.Begin);
}
else
{
throw new Exception($"Unable to open media '{videoPath}' in '{mediaOpenTimeout}'");
}
}
finally
{
media.Close();
}
return memory;
}
private void SetPosition(MediaPlayer player)
{
if (player.NaturalDuration.HasTimeSpan)
{
TimeSpan naturalTime = player.NaturalDuration.TimeSpan;
if (naturalTime < Time)
{
if (naturalTime > FallbackTime)
{
player.Position = FallbackTime;
}
}
else
{
player.Position = Time;
}
}
}
private void Media_MediaOpened(object sender, EventArgs e)
{
waitHandle.Set();
}
public void Dispose()
{
waitHandle.Dispose();
}
}
MediaElement is basically a visual control for hosting media content:
Represents a control that contains audio and/or video
You need a UI-less MediaPlayer. Just don't forget to call Open method or MediaOpened won't fire.
I am trying to build a collapsible panel by having a control with a header and a panel. When someone clicks on the header the panel should collapse/expand.
However when I add another control inside the body of the collapsible panel which is a Panel, the width and height seem to be locked therefore they cannot be changed. My understanding was disabling AutoSize would allow me to freely resize the panel but obviously not. What am I missing here?
internal sealed class CollapsiblePanel : Control
{
private const string DownIconBase64 = "AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAAAABAREQCQEREBEBERAJAREQCQEREBEBERAJAREQEQEREAkBERAJAREQEQEREAkBERARAREQCQEREAkBERARAREQCQEREAkBERARAREQEQEREBEBERARAREQEQEREBAYHByQGBwckQEREBEBERARAREQEQEREBEBERARAREQEQEREBEBERAJAREQEQEREBEBERARAREQEQEREBAcHByQBAgLhAQIC4QYHByRAREQEQEREBEBERARAREQEQEREBEBERAJAREQCQEREBEBERARAREQEQEREBAYHByQBAgLhAQIC/wEBAf8BAgLhBgcHJEBERARAREQEQEREBEBERARAREQCQEREAkBERARAREQEQEREBAYGBiQBAgLhAQIC/wECAt0BAgLfAQEB/wECAuEGBwckQEREBEBERARAREQEQEREBEBERAJAREQEQEREBEBERAQBAgLhAQEB/wECAt8HBwciBwcHIAECAt8BAQH/AQIC4UBERARAREQEQEREBEBERAJAREQCQEREBEBERARAREQEAQEB/wECAuEGBwciQEREBEBERAQHBwcgAQIC3wEBAf9AREQEQEREBEBERARAREQEQEREAkBERARAREQEQEREBAECAuEGBwckQEREBAYGBiQGBwckQEREBAcHByABAgLfQEREBEBERARAREQEQEREAkBERAJAREQEQEREBEBERAQGBwckQEREBAYGBiQBAgLhAQIC4QYHByRAREQEBwcHIEBERARAREQEQEREBEBERAJAREQCQEREBEBERARAREQEQEREBAYHByQBAgLhAQEB/wECAv8BAgLhBgcHJEBERARAREQEQEREBEBERARAREQEQEREAkBERARAREQEQEREBAcHByQBAgLhAQIC/wECAt0BAgLdAQIC/wECAuEGBwckQEREBEBERARAREQEQEREAkBERAJAREQEQEREBEBERAQBAgLhAQIC/wECAt8HBwciBwgIIAECAt0BAgL/AQIC4UBERARAREQEQEREBEBERARAREQCQEREBEBERARAREQEAQIC/wECAt8HCAgiQEREBEBERAQHCAggAQIC3QECAv9AREQEQEREBEBERARAREQCQEREAkBERARAREQEQEREBAECAuEHBwckQEREBEBERARAREQEQEREBAcICCABAgLdQEREBEBERARAREQEQEREAkBERAJAREQEQEREBEBERAQGBgYkQEREBEBERARAREQEQEREBEBERARAREQEBwgIIEBERARAREQEQEREBEBERARAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
private const string UpIconBase64 = "AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAAAABAREQCQEREBEBERAJAREQCQEREBEBERAJAREQEQEREAkBERAJAREQEQEREAkBERARAREQCQEREAkBERARAREQCQEREAkBERARAREQEQEREBAcHByBAREQEQEREBEBERARAREQEQEREBEBERAQGBgYkQEREBEBERARAREQEQEREBEBERAJAREQEQEREBEBERAQBAgLdBwcHIEBERARAREQEQEREBEBERAQGBwckAQIC4UBERARAREQEQEREBEBERAJAREQCQEREBEBERARAREQEAQIC/wECAt0HBwcgQEREBEBERAQHBwciAQIC3wEBAf9AREQEQEREBEBERARAREQCQEREAkBERARAREQEQEREBAECAuEBAgL/AQIC3QcHByAHCAgiAQIC3wECAv8BAgLhQEREBEBERARAREQEQEREBEBERAJAREQEQEREBEBERAQHBwckAQIC4QECAv8BAgLdAQIC3QECAv8BAgLhBgcHJEBERARAREQEQEREBEBERAJAREQCQEREBEBERARAREQEQEREBAcHByQBAgLhAQIC/wECAv8BAgLhBwcHJEBERARAREQEQEREBEBERARAREQEQEREAkBERARAREQEQEREBAcICCBAREQEBwcHJAECAuEBAgLhBgcHJEBERAQHBwckQEREBEBERARAREQEQEREAkBERAJAREQEQEREBEBERAQBAgLdBwgIIEBERAQHBwckBgYGJEBERAQHBwckAQIC4UBERARAREQEQEREBEBERAJAREQCQEREBEBERARAREQEAQEB/wECAt0HCAggQEREBEBERAQGBwciAQIC3wECAv9AREQEQEREBEBERARAREQEQEREAkBERARAREQEQEREBAECAuMBAQH/AQIC3QcICCAHBwciAQIC3wEBAf8BAgLhQEREBEBERARAREQEQEREAkBERAJAREQEQEREBEBERAQGBgYmAQIC4wEBAf8BAgLdAQIC3QEBAf8BAgLhBgYGJEBERARAREQEQEREBEBERARAREQCQEREBEBERARAREQEQEREBAYGBiYBAgLjAQEB/wECAv8BAgLhBgYGJEBERARAREQEQEREBEBERARAREQCQEREAkBERARAREQEQEREBEBERARAREQEBgYGJgECAuMBAgLhBgcHJEBERARAREQEQEREBEBERARAREQEQEREAkBERAJAREQEQEREBEBERARAREQEQEREBEBERAQGBgYmBwcHJEBERARAREQEQEREBEBERARAREQEQEREBEBERARAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCQEREAkBERAJAREQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
private static readonly Image DownIcon = DownIconBase64.ToImage();
private static readonly Image UpIcon = UpIconBase64.ToImage();
private readonly Button _headerButton;
private readonly Panel _body;
private int _width, _height;
private bool _isCollapsed;
public CollapsiblePanel(string name)
{
_body = new Panel { Dock = DockStyle.Fill, AutoSize = false };
_headerButton = new Button
{
Dock = DockStyle.Top,
Text = name,
TextAlign = ContentAlignment.MiddleLeft,
Image = DownIcon,
ImageAlign = ContentAlignment.MiddleRight,
UseVisualStyleBackColor = false
};
_headerButton.Click += (_, __) =>
{
if (_isCollapsed)
{
Expand();
}
else
{
Collapse();
}
};
Controls.Add(_body);
Controls.Add(_headerButton);
ControlAdded += (_, eArgs) => { _body.Controls.Add(eArgs.Control); };
ControlRemoved += (_, eArgs) => { _body.Controls.Remove(eArgs.Control); };
}
public void Collapse()
{
_width = _body.Width;
_height = _body.Height;
_body.Width = 0;
_body.Height = 0;
_headerButton.Image = UpIcon;
_isCollapsed = true;
}
public void Expand()
{
_body.Width = _width;
_body.Height = _height;
_headerButton.Image = DownIcon;
_isCollapsed = false;
}
}
internal static class Extensions
{
/// <summary>
/// Converts the given <paramref name="base64Input"/> to <see cref="Image"/>.
/// </summary>
public static Image ToImage(this string base64Input)
{
var imageBytes = Convert.FromBase64String(base64Input);
using (var ms = new MemoryStream(imageBytes, 0, imageBytes.Length))
{
return Image.FromStream(ms, true);
}
}
}
The Fill docking makes it to fix the size. You may want to change the Fill property when you collapse or expand.
public void Collapse()
{
_width = _body.Width;
_height = _body.Height;
_body.Dock = DockStyle.None;
_body.Width = 0;
_body.Height = 0;
_headerButton.Image = UpIcon;
_isCollapsed = true;
}
public void Expand()
{
_body.Width = _width;
_body.Height = _height;
_body.Dock = DockStyle.Fill;
_headerButton.Image = DownIcon;
_isCollapsed = false;
}
It is a lot easier to work just with the Visible property like
public void Collapse()
{
_body.Visible = false;
_headerButton.Image = UpIcon;
_isCollapsed = true;
}
public void Expand()
{
_body.Visible = true;
_headerButton.Image = DownIcon;
_isCollapsed = false;
}
No need to keep around _width, _height values to restore the size.
Set Dock property of your panel (_body) to None