I'm using the MonthCalendar control and want to programmatically select a date range. When I do so the control doesn't paint properly if Application.EnableVisualStyles() has been called. This is a known issue according to MSDN.
Using the MonthCalendar with visual
styles enabled will cause a selection
range for the MonthCalendar control to
not paint correctly
(from: http://msdn.microsoft.com/en-us/library/system.windows.forms.monthcalendar.aspx)
Is there really no fix for this other than not calling EnableVisualStyles? This seems to make that particular control entirely useless for a range of applications and a rather glaring oversight from my perspective.
While looking for a solution to the same problem, I first encountered this question here, but later I discovered a blog entry by Nicke Andersson. which I found very helpful.
Here is what I made of Nicke's example:
public class MonthCalendarEx : System.Windows.Forms.MonthCalendar
{
private int _offsetX;
private int _offsetY;
private int _dayBoxWidth;
private int _dayBoxHeight;
private bool _repaintSelectedDays = false;
public MonthCalendarEx() : base()
{
OnSizeChanged(null, null);
this.SizeChanged += OnSizeChanged;
this.DateChanged += OnSelectionChanged;
this.DateSelected += OnSelectionChanged;
}
protected static int WM_PAINT = 0x000F;
protected override void WndProc(ref System.Windows.Forms.Message m)
{
base.WndProc(ref m);
if (m.Msg == WM_PAINT)
{
Graphics graphics = Graphics.FromHwnd(this.Handle);
PaintEventArgs pe = new PaintEventArgs(
graphics, new Rectangle(0, 0, this.Width, this.Height));
OnPaint(pe);
}
}
private void OnSelectionChanged(object sender, EventArgs e)
{
_repaintSelectedDays = true;
}
private void OnSizeChanged(object sender, EventArgs e)
{
_offsetX = 0;
_offsetY = 0;
// determine Y offset of days area
while (
HitTest(Width / 2, _offsetY).HitArea != HitArea.PrevMonthDate &&
HitTest(Width / 2, _offsetY).HitArea != HitArea.Date)
{
_offsetY++;
}
// determine X offset of days area
while (HitTest(_offsetX, Height / 2).HitArea != HitArea.Date)
{
_offsetX++;
}
// determine width of a single day box
_dayBoxWidth = 0;
DateTime dt1 = HitTest(Width / 2, _offsetY).Time;
while (HitTest(Width / 2, _offsetY + _dayBoxHeight).Time == dt1)
{
_dayBoxHeight++;
}
// determine height of a single day box
_dayBoxWidth = 0;
DateTime dt2 = HitTest(_offsetX, Height / 2).Time;
while (HitTest(_offsetX + _dayBoxWidth, Height / 2).Time == dt2)
{
_dayBoxWidth++;
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (_repaintSelectedDays)
{
Graphics graphics = e.Graphics;
SelectionRange calendarRange = GetDisplayRange(false);
Rectangle currentDayFrame = new Rectangle(
-1, -1, _dayBoxWidth, _dayBoxHeight);
DateTime current = SelectionStart;
while (current <= SelectionEnd)
{
Rectangle currentDayRectangle;
using (Brush selectionBrush = new SolidBrush(
Color.FromArgb(
255, System.Drawing.SystemColors.ActiveCaption)))
{
TimeSpan span = current.Subtract(calendarRange.Start);
int row = span.Days / 7;
int col = span.Days % 7;
currentDayRectangle = new Rectangle(
_offsetX + (col + (ShowWeekNumbers ? 1 : 0)) * _dayBoxWidth,
_offsetY + row * _dayBoxHeight,
_dayBoxWidth,
_dayBoxHeight);
graphics.FillRectangle(selectionBrush, currentDayRectangle);
}
TextRenderer.DrawText(
graphics,
current.Day.ToString(),
Font,
currentDayRectangle,
System.Drawing.SystemColors.ActiveCaptionText,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
if (current == this.TodayDate)
{
currentDayFrame = currentDayRectangle;
}
current = current.AddDays(1);
}
if (currentDayFrame.X > 0)
{
graphics.DrawRectangle(new Pen(
new SolidBrush(Color.Red)), currentDayFrame);
}
_repaintSelectedDays = false;
}
}
}
I found a small problem in Mark Cranness's code above: On XP systems that have visual styles disabled entirely, Application.RenderWithVisualStyles is then set to False even when Application.EnableVisualStyles() is called.
So the custom paint code does not run at all in that case. To fix it, I changed the first line of the FixVisualStylesMonthCalendar constructor to
if (Application.VisualStyleState != System.Windows.Forms.VisualStyles.VisualStyleState.NoneEnabled &&
Environment.OSVersion.Version < new Version(6, 0))
Entire code is at the bottom of this answer.
I could not find any way to comment on the answer itself. Credits for below code go to the original author - (If he or anyone can verify this answer and update it I would be happy to remove this one)
/// <summary>
/// When Visual Styles are enabled on Windows XP, the MonthCalendar.SelectionRange
/// does not paint correctly when more than one date is selected.
/// See: http://msdn.microsoft.com/en-us/library/5d1acks5(VS.80).aspx
/// "Additionally, if you enable visual styles on some controls, the control might display incorrectly
/// in certain situations. These include the MonthCalendar control with a selection range set...
/// This class fixes that problem.
/// </summary>
/// <remarks>Author: Mark Cranness - PatronBase Limited.</remarks>
public class FixVisualStylesMonthCalendar : System.Windows.Forms.MonthCalendar
{
/// <summary>
/// The width of a single cell (date) in the calendar.
/// </summary>
private int dayCellWidth;
/// <summary>
/// The height of a single cell (date) in the calendar.
/// </summary>
private int dayCellHeight;
/// <summary>
/// The calendar first day of the week actually used.
/// </summary>
private DayOfWeek calendarFirstDayOfWeek;
/// <summary>
/// Only repaint when VisualStyles enabled on Windows XP.
/// </summary>
private bool repaintSelectionRange = false;
/// <summary>
/// A MonthCalendar class that fixes SelectionRange painting problems
/// on Windows XP when Visual Styles is enabled.
/// </summary>
public FixVisualStylesMonthCalendar()
{
if (Application.VisualStyleState != System.Windows.Forms.VisualStyles.VisualStyleState.NoneEnabled && //Application.RenderWithVisualStyles &&
Environment.OSVersion.Version < new Version(6, 0))
{
// If Visual Styles are enabled, and XP, then fix-up the painting of SelectionRange
this.repaintSelectionRange = true;
this.OnSizeChanged(this, EventArgs.Empty);
this.SizeChanged += new EventHandler(this.OnSizeChanged);
}
}
/// <summary>
/// The WM_PAINT message is sent to make a request to paint a portion of a window.
/// </summary>
public const int WM_PAINT = 0x000F;
/// <summary>
/// Override WM_PAINT to repaint the selection range.
/// </summary>
[System.Diagnostics.DebuggerStepThroughAttribute()]
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == WM_PAINT
&& !this.DesignMode
&& this.repaintSelectionRange)
{
// MonthCalendar is ControlStyles.UserPaint=false => Paint event is not raised
this.RepaintSelectionRange(ref m);
}
}
/// <summary>
/// Repaint the SelectionRange.
/// </summary>
private void RepaintSelectionRange(ref Message m)
{
using (Graphics graphics = this.CreateGraphics())
using (Brush backBrush
= new SolidBrush(graphics.GetNearestColor(this.BackColor)))
using (Brush selectionBrush
= new SolidBrush(graphics.GetNearestColor(SystemColors.ActiveCaption)))
{
Rectangle todayFrame = Rectangle.Empty;
// For each day in SelectionRange...
for (DateTime selectionDate = this.SelectionStart;
selectionDate <= this.SelectionEnd;
selectionDate = selectionDate.AddDays(1))
{
Rectangle selectionDayRectangle = this.GetSelectionDayRectangle(selectionDate);
if (selectionDayRectangle.IsEmpty) continue;
if (selectionDate.Date == this.TodayDate)
{
todayFrame = selectionDayRectangle;
}
// Paint as 'selected' a little smaller than the whole rectangle
Rectangle highlightRectangle = Rectangle.Inflate(selectionDayRectangle, 0, -2);
if (selectionDate == this.SelectionStart)
{
highlightRectangle.X += 2;
highlightRectangle.Width -= 2;
}
if (selectionDate == this.SelectionEnd)
{
highlightRectangle.Width -= 2;
}
// Paint background, selection and day-of-month text
graphics.FillRectangle(backBrush, selectionDayRectangle);
graphics.FillRectangle(selectionBrush, highlightRectangle);
TextRenderer.DrawText(
graphics,
selectionDate.Day.ToString(),
this.Font,
selectionDayRectangle,
SystemColors.ActiveCaptionText,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
if (this.ShowTodayCircle && !todayFrame.IsEmpty)
{
// Redraw the ShowTodayCircle (square) that we painted over above
using (Pen redPen = new Pen(Color.Red))
{
todayFrame.Width--;
todayFrame.Height--;
graphics.DrawRectangle(redPen, todayFrame);
}
}
}
}
/// <summary>
/// When displayed dates changed, clear the cached month locations.
/// </summary>
private SelectionRange previousDisplayedDates = new SelectionRange();
/// <summary>
/// Gets a graphics Rectangle for the area corresponding to a single date on the calendar.
/// </summary>
private Rectangle GetSelectionDayRectangle(DateTime selectionDateTime)
{
// Handle the leading and trailing dates from the previous and next months
SelectionRange allDisplayedDates = this.GetDisplayRange(false);
SelectionRange fullMonthDates = this.GetDisplayRange(true);
int adjust1Week;
DateTime selectionDate = selectionDateTime.Date;
if (selectionDate < allDisplayedDates.Start
|| selectionDate > allDisplayedDates.End)
{
// Selection Date is not displayed on calendar
return Rectangle.Empty;
}
else if (selectionDate < fullMonthDates.Start)
{
// Selection Date is trailing from the previous partial month
selectionDate = selectionDate.AddDays(7);
adjust1Week = -1;
}
else if (selectionDate > fullMonthDates.End)
{
// Selection Date is leading from the next partial month
selectionDate = selectionDate.AddDays(-14);
adjust1Week = +2;
}
else
{
// A mainline date
adjust1Week = 0;
}
// Discard cached month locations when calendar moves
if (this.previousDisplayedDates.Start != allDisplayedDates.Start
|| this.previousDisplayedDates.End != allDisplayedDates.End)
{
this.DiscardCachedMonthDateAreaLocations();
this.previousDisplayedDates.Start = allDisplayedDates.Start;
this.previousDisplayedDates.End = allDisplayedDates.End;
}
Point monthDateAreaLocation = this.GetMonthDateAreaLocation(selectionDate);
if (monthDateAreaLocation.IsEmpty) return Rectangle.Empty;
DayOfWeek monthFirstDayOfWeek = (new DateTime(selectionDate.Year, selectionDate.Month, 1)).DayOfWeek;
int dayOfWeekAdjust = (int)monthFirstDayOfWeek - (int)this.calendarFirstDayOfWeek;
if (dayOfWeekAdjust < 0) dayOfWeekAdjust += 7;
int row = (selectionDate.Day - 1 + dayOfWeekAdjust) / 7;
int col = (selectionDate.Day - 1 + dayOfWeekAdjust) % 7;
row += adjust1Week;
return new Rectangle(
monthDateAreaLocation.X + col * this.dayCellWidth,
monthDateAreaLocation.Y + row * this.dayCellHeight,
this.dayCellWidth,
this.dayCellHeight);
}
/// <summary>
/// Cached calendar location from the last lookup.
/// </summary>
private Point[] cachedMonthDateAreaLocation = new Point[13];
/// <summary>
/// Discard the cached month locations when calendar moves.
/// </summary>
private void DiscardCachedMonthDateAreaLocations()
{
for (int i = 0; i < 13; i++) this.cachedMonthDateAreaLocation[i] = Point.Empty;
}
/// <summary>
/// Gets the graphics location (x,y point) of the top left of the
/// calendar date area for the month containing the specified date.
/// </summary>
private Point GetMonthDateAreaLocation(DateTime selectionDate)
{
Point monthDateAreaLocation = this.cachedMonthDateAreaLocation[selectionDate.Month];
HitTestInfo hitInfo;
if (!monthDateAreaLocation.IsEmpty
&& (hitInfo = this.HitTest(monthDateAreaLocation.X, monthDateAreaLocation.Y + this.dayCellHeight))
.HitArea == HitArea.Date
&& hitInfo.Time.Year == selectionDate.Year
&& hitInfo.Time.Month == selectionDate.Month)
{
// Use previously cached lookup
return monthDateAreaLocation;
}
else
{
// Assume the worst (Error: empty)
monthDateAreaLocation = this.cachedMonthDateAreaLocation[selectionDate.Month] = Point.Empty;
Point monthDataAreaPoint = this.GetMonthDateAreaMiddle(selectionDate);
if (monthDataAreaPoint.IsEmpty) return Point.Empty;
// Move left from the middle to find the left edge of the Date area
monthDateAreaLocation.X = monthDataAreaPoint.X--;
HitTestInfo hitInfo1, hitInfo2;
while ((hitInfo1 = this.HitTest(monthDataAreaPoint.X, monthDataAreaPoint.Y))
.HitArea == HitArea.Date
&& hitInfo1.Time.Month == selectionDate.Month
|| (hitInfo2 = this.HitTest(monthDataAreaPoint.X, monthDataAreaPoint.Y + this.dayCellHeight))
.HitArea == HitArea.Date
&& hitInfo2.Time.Month == selectionDate.Month)
{
monthDateAreaLocation.X = monthDataAreaPoint.X--;
if (monthDateAreaLocation.X < 0) return Point.Empty; // Error: bail
}
// Move up from the last column to find the top edge of the Date area
int monthLastDayOfWeekX = monthDateAreaLocation.X + (this.dayCellWidth * 7 * 13) / 14;
monthDateAreaLocation.Y = monthDataAreaPoint.Y--;
while (this.HitTest(monthLastDayOfWeekX, monthDataAreaPoint.Y).HitArea == HitArea.Date)
{
monthDateAreaLocation.Y = monthDataAreaPoint.Y--;
if (monthDateAreaLocation.Y < 0) return Point.Empty; // Error: bail
}
// Got it
this.cachedMonthDateAreaLocation[selectionDate.Month] = monthDateAreaLocation;
return monthDateAreaLocation;
}
}
/// <summary>
/// Paranoid fudge/wobble of the GetMonthDateAreaMiddle in case
/// our first estimate to hit the month misses.
/// (Needed? perhaps not.)
/// </summary>
private static Point[] searchSpiral = {
new Point( 0, 0),
new Point(-1,+1), new Point(+1,+1), new Point(+1,-1), new Point(-1,-1),
new Point(-2,+2), new Point(+2,+2), new Point(+2,-2), new Point(-2,-2)
};
/// <summary>
/// Gets a point somewhere inside the calendar date area of
/// the month containing the given selection date.
/// </summary>
/// <remarks>The point returned will be HitArea.Date, and match the year and
/// month of the selection date; otherwise it will be Point.Empty.</remarks>
private Point GetMonthDateAreaMiddle(DateTime selectionDate)
{
// Iterate over all displayed months, and a search spiral (needed? perhaps not)
for (int dimX = 1; dimX <= this.CalendarDimensions.Width; dimX++)
{
for (int dimY = 1; dimY <= this.CalendarDimensions.Height; dimY++)
{
foreach (Point search in searchSpiral)
{
Point monthDateAreaMiddle = new Point(
((dimX - 1) * 2 + 1) * this.Width / (2 * this.CalendarDimensions.Width)
+ this.dayCellWidth * search.X,
((dimY - 1) * 2 + 1) * this.Height / (2 * this.CalendarDimensions.Height)
+ this.dayCellHeight * search.Y);
HitTestInfo hitInfo = this.HitTest(monthDateAreaMiddle);
if (hitInfo.HitArea == HitArea.Date)
{
// Got the Date Area of the month
if (hitInfo.Time.Year == selectionDate.Year
&& hitInfo.Time.Month == selectionDate.Month)
{
// For the correct month
return monthDateAreaMiddle;
}
else
{
// Keep looking in the other months
break;
}
}
}
}
}
return Point.Empty; // Error: not found
}
/// <summary>
/// When this MonthCalendar is resized, recalculate the size of a day cell.
/// </summary>
private void OnSizeChanged(object sender, EventArgs e)
{
// Discard previous cached Month Area Location
DiscardCachedMonthDateAreaLocations();
this.dayCellWidth = this.dayCellHeight = 0;
// Without this, the repaint sometimes does not happen...
this.Invalidate();
// Determine Y offset of days area
int middle = this.Width / (2 * this.CalendarDimensions.Width);
int dateAreaTop = 0;
while (this.HitTest(middle, dateAreaTop).HitArea != HitArea.PrevMonthDate
&& this.HitTest(middle, dateAreaTop).HitArea != HitArea.Date)
{
dateAreaTop++;
if (dateAreaTop > this.ClientSize.Height) return; // Error: bail
}
// Determine height of a single day box
int dayCellHeight = 1;
DateTime dayCellTime = this.HitTest(middle, dateAreaTop).Time;
while (this.HitTest(middle, dateAreaTop + dayCellHeight).Time == dayCellTime)
{
dayCellHeight++;
}
// Determine X offset of days area
middle = this.Height / (2 * this.CalendarDimensions.Height);
int dateAreaLeft = 0;
while (this.HitTest(dateAreaLeft, middle).HitArea != HitArea.Date)
{
dateAreaLeft++;
if (dateAreaLeft > this.ClientSize.Width) return; // Error: bail
}
// Determine width of a single day box
int dayCellWidth = 1;
dayCellTime = this.HitTest(dateAreaLeft, middle).Time;
while (this.HitTest(dateAreaLeft + dayCellWidth, middle).Time == dayCellTime)
{
dayCellWidth++;
}
// Record day box size and actual first day of the month used
this.calendarFirstDayOfWeek = dayCellTime.DayOfWeek;
this.dayCellWidth = dayCellWidth;
this.dayCellHeight = dayCellHeight;
}
}
Here is a version that does work when more than one month is displayed (CalendarDimensions != (1,1)), and fixes some other problems also:
/// <summary>
/// When Visual Styles are enabled on Windows XP, the MonthCalendar.SelectionRange
/// does not paint correctly when more than one date is selected.
/// See: http://msdn.microsoft.com/en-us/library/5d1acks5(VS.80).aspx
/// "Additionally, if you enable visual styles on some controls, the control might display incorrectly
/// in certain situations. These include the MonthCalendar control with a selection range set...
/// This class fixes that problem.
/// </summary>
/// <remarks>Author: Mark Cranness</remarks>
public class FixVisualStylesMonthCalendar : System.Windows.Forms.MonthCalendar {
/// <summary>
/// The width of a single cell (date) in the calendar.
/// </summary>
private int dayCellWidth;
/// <summary>
/// The height of a single cell (date) in the calendar.
/// </summary>
private int dayCellHeight;
/// <summary>
/// The calendar first day of the week actually used.
/// </summary>
private DayOfWeek calendarFirstDayOfWeek;
/// <summary>
/// Only repaint when VisualStyles enabled on Windows XP.
/// </summary>
private bool repaintSelectionRange = false;
/// <summary>
/// A MonthCalendar class that fixes SelectionRange painting problems
/// on Windows XP when Visual Styles is enabled.
/// </summary>
public FixVisualStylesMonthCalendar() {
if (Application.RenderWithVisualStyles
&& Environment.OSVersion.Version < new Version(6, 0)) {
// If Visual Styles are enabled, and XP, then fix-up the painting of SelectionRange
this.repaintSelectionRange = true;
this.OnSizeChanged(this, EventArgs.Empty);
this.SizeChanged += new EventHandler(this.OnSizeChanged);
}
}
/// <summary>
/// The WM_PAINT message is sent to make a request to paint a portion of a window.
/// </summary>
public const int WM_PAINT = 0x000F;
/// <summary>
/// Override WM_PAINT to repaint the selection range.
/// </summary>
[System.Diagnostics.DebuggerStepThroughAttribute()]
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == WM_PAINT
&& !this.DesignMode
&& this.repaintSelectionRange) {
// MonthCalendar is ControlStyles.UserPaint=false => Paint event is not raised
this.RepaintSelectionRange(ref m);
}
}
/// <summary>
/// Repaint the SelectionRange.
/// </summary>
private void RepaintSelectionRange(ref Message m) {
using (Graphics graphics = this.CreateGraphics())
using (Brush backBrush
= new SolidBrush(graphics.GetNearestColor(this.BackColor)))
using (Brush selectionBrush
= new SolidBrush(graphics.GetNearestColor(SystemColors.ActiveCaption))) {
Rectangle todayFrame = Rectangle.Empty;
// For each day in SelectionRange...
for (DateTime selectionDate = this.SelectionStart;
selectionDate <= this.SelectionEnd;
selectionDate = selectionDate.AddDays(1)) {
Rectangle selectionDayRectangle = this.GetSelectionDayRectangle(selectionDate);
if (selectionDayRectangle.IsEmpty) continue;
if (selectionDate.Date == this.TodayDate) {
todayFrame = selectionDayRectangle;
}
// Paint as 'selected' a little smaller than the whole rectangle
Rectangle highlightRectangle = Rectangle.Inflate(selectionDayRectangle, 0, -2);
if (selectionDate == this.SelectionStart) {
highlightRectangle.X += 2;
highlightRectangle.Width -= 2;
}
if (selectionDate == this.SelectionEnd) {
highlightRectangle.Width -= 2;
}
// Paint background, selection and day-of-month text
graphics.FillRectangle(backBrush, selectionDayRectangle);
graphics.FillRectangle(selectionBrush, highlightRectangle);
TextRenderer.DrawText(
graphics,
selectionDate.Day.ToString(),
this.Font,
selectionDayRectangle,
SystemColors.ActiveCaptionText,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
if (this.ShowTodayCircle && !todayFrame.IsEmpty) {
// Redraw the ShowTodayCircle (square) that we painted over above
using (Pen redPen = new Pen(Color.Red)) {
todayFrame.Width--;
todayFrame.Height--;
graphics.DrawRectangle(redPen, todayFrame);
}
}
}
}
/// <summary>
/// When displayed dates changed, clear the cached month locations.
/// </summary>
private SelectionRange previousDisplayedDates = new SelectionRange();
/// <summary>
/// Gets a graphics Rectangle for the area corresponding to a single date on the calendar.
/// </summary>
private Rectangle GetSelectionDayRectangle(DateTime selectionDateTime) {
// Handle the leading and trailing dates from the previous and next months
SelectionRange allDisplayedDates = this.GetDisplayRange(false);
SelectionRange fullMonthDates = this.GetDisplayRange(true);
int adjust1Week;
DateTime selectionDate = selectionDateTime.Date;
if (selectionDate < allDisplayedDates.Start
|| selectionDate > allDisplayedDates.End) {
// Selection Date is not displayed on calendar
return Rectangle.Empty;
} else if (selectionDate < fullMonthDates.Start) {
// Selection Date is trailing from the previous partial month
selectionDate = selectionDate.AddDays(7);
adjust1Week = -1;
} else if (selectionDate > fullMonthDates.End) {
// Selection Date is leading from the next partial month
selectionDate = selectionDate.AddDays(-14);
adjust1Week = +2;
} else {
// A mainline date
adjust1Week = 0;
}
// Discard cached month locations when calendar moves
if (this.previousDisplayedDates.Start != allDisplayedDates.Start
|| this.previousDisplayedDates.End != allDisplayedDates.End) {
this.DiscardCachedMonthDateAreaLocations();
this.previousDisplayedDates.Start = allDisplayedDates.Start;
this.previousDisplayedDates.End = allDisplayedDates.End;
}
Point monthDateAreaLocation = this.GetMonthDateAreaLocation(selectionDate);
if (monthDateAreaLocation.IsEmpty) return Rectangle.Empty;
DayOfWeek monthFirstDayOfWeek = (new DateTime(selectionDate.Year, selectionDate.Month, 1)).DayOfWeek;
int dayOfWeekAdjust = (int)monthFirstDayOfWeek - (int)this.calendarFirstDayOfWeek;
if (dayOfWeekAdjust < 0) dayOfWeekAdjust += 7;
int row = (selectionDate.Day - 1 + dayOfWeekAdjust) / 7;
int col = (selectionDate.Day - 1 + dayOfWeekAdjust) % 7;
row += adjust1Week;
return new Rectangle(
monthDateAreaLocation.X + col * this.dayCellWidth,
monthDateAreaLocation.Y + row * this.dayCellHeight,
this.dayCellWidth,
this.dayCellHeight);
}
/// <summary>
/// Cached calendar location from the last lookup.
/// </summary>
private Point[] cachedMonthDateAreaLocation = new Point[13];
/// <summary>
/// Discard the cached month locations when calendar moves.
/// </summary>
private void DiscardCachedMonthDateAreaLocations() {
for (int i = 0; i < 13; i++) this.cachedMonthDateAreaLocation[i] = Point.Empty;
}
/// <summary>
/// Gets the graphics location (x,y point) of the top left of the
/// calendar date area for the month containing the specified date.
/// </summary>
private Point GetMonthDateAreaLocation(DateTime selectionDate) {
Point monthDateAreaLocation = this.cachedMonthDateAreaLocation[selectionDate.Month];
HitTestInfo hitInfo;
if (!monthDateAreaLocation.IsEmpty
&& (hitInfo = this.HitTest(monthDateAreaLocation.X, monthDateAreaLocation.Y + this.dayCellHeight))
.HitArea == HitArea.Date
&& hitInfo.Time.Year == selectionDate.Year
&& hitInfo.Time.Month == selectionDate.Month) {
// Use previously cached lookup
return monthDateAreaLocation;
} else {
// Assume the worst (Error: empty)
monthDateAreaLocation = this.cachedMonthDateAreaLocation[selectionDate.Month] = Point.Empty;
Point monthDataAreaPoint = this.GetMonthDateAreaMiddle(selectionDate);
if (monthDataAreaPoint.IsEmpty) return Point.Empty;
// Move left from the middle to find the left edge of the Date area
monthDateAreaLocation.X = monthDataAreaPoint.X--;
HitTestInfo hitInfo1, hitInfo2;
while ((hitInfo1 = this.HitTest(monthDataAreaPoint.X, monthDataAreaPoint.Y))
.HitArea == HitArea.Date
&& hitInfo1.Time.Month == selectionDate.Month
|| (hitInfo2 = this.HitTest(monthDataAreaPoint.X, monthDataAreaPoint.Y + this.dayCellHeight))
.HitArea == HitArea.Date
&& hitInfo2.Time.Month == selectionDate.Month) {
monthDateAreaLocation.X = monthDataAreaPoint.X--;
if (monthDateAreaLocation.X < 0) return Point.Empty; // Error: bail
}
// Move up from the last column to find the top edge of the Date area
int monthLastDayOfWeekX = monthDateAreaLocation.X + (this.dayCellWidth * 7 * 13) / 14;
monthDateAreaLocation.Y = monthDataAreaPoint.Y--;
while (this.HitTest(monthLastDayOfWeekX, monthDataAreaPoint.Y).HitArea == HitArea.Date) {
monthDateAreaLocation.Y = monthDataAreaPoint.Y--;
if (monthDateAreaLocation.Y < 0) return Point.Empty; // Error: bail
}
// Got it
this.cachedMonthDateAreaLocation[selectionDate.Month] = monthDateAreaLocation;
return monthDateAreaLocation;
}
}
/// <summary>
/// Paranoid fudge/wobble of the GetMonthDateAreaMiddle in case
/// our first estimate to hit the month misses.
/// (Needed? perhaps not.)
/// </summary>
private static Point[] searchSpiral = {
new Point( 0, 0),
new Point(-1,+1), new Point(+1,+1), new Point(+1,-1), new Point(-1,-1),
new Point(-2,+2), new Point(+2,+2), new Point(+2,-2), new Point(-2,-2)
};
/// <summary>
/// Gets a point somewhere inside the calendar date area of
/// the month containing the given selection date.
/// </summary>
/// <remarks>The point returned will be HitArea.Date, and match the year and
/// month of the selection date; otherwise it will be Point.Empty.</remarks>
private Point GetMonthDateAreaMiddle(DateTime selectionDate) {
// Iterate over all displayed months, and a search spiral (needed? perhaps not)
for (int dimX = 1; dimX <= this.CalendarDimensions.Width; dimX++) {
for (int dimY = 1; dimY <= this.CalendarDimensions.Height; dimY++) {
foreach (Point search in searchSpiral) {
Point monthDateAreaMiddle = new Point(
((dimX - 1) * 2 + 1) * this.Width / (2 * this.CalendarDimensions.Width)
+ this.dayCellWidth * search.X,
((dimY - 1) * 2 + 1) * this.Height / (2 * this.CalendarDimensions.Height)
+ this.dayCellHeight * search.Y);
HitTestInfo hitInfo = this.HitTest(monthDateAreaMiddle);
if (hitInfo.HitArea == HitArea.Date) {
// Got the Date Area of the month
if (hitInfo.Time.Year == selectionDate.Year
&& hitInfo.Time.Month == selectionDate.Month) {
// For the correct month
return monthDateAreaMiddle;
} else {
// Keep looking in the other months
break;
}
}
}
}
}
return Point.Empty; // Error: not found
}
/// <summary>
/// When this MonthCalendar is resized, recalculate the size of a day cell.
/// </summary>
private void OnSizeChanged(object sender, EventArgs e) {
// Discard previous cached Month Area Location
DiscardCachedMonthDateAreaLocations();
this.dayCellWidth = this.dayCellHeight = 0;
// Without this, the repaint sometimes does not happen...
this.Invalidate();
// Determine Y offset of days area
int middle = this.Width / (2 * this.CalendarDimensions.Width);
int dateAreaTop = 0;
while (this.HitTest(middle, dateAreaTop).HitArea != HitArea.PrevMonthDate
&& this.HitTest(middle, dateAreaTop).HitArea != HitArea.Date) {
dateAreaTop++;
if (dateAreaTop > this.ClientSize.Height) return; // Error: bail
}
// Determine height of a single day box
int dayCellHeight = 1;
DateTime dayCellTime = this.HitTest(middle, dateAreaTop).Time;
while (this.HitTest(middle, dateAreaTop + dayCellHeight).Time == dayCellTime) {
dayCellHeight++;
}
// Determine X offset of days area
middle = this.Height / (2 * this.CalendarDimensions.Height);
int dateAreaLeft = 0;
while (this.HitTest(dateAreaLeft, middle).HitArea != HitArea.Date) {
dateAreaLeft++;
if (dateAreaLeft > this.ClientSize.Width) return; // Error: bail
}
// Determine width of a single day box
int dayCellWidth = 1;
dayCellTime = this.HitTest(dateAreaLeft, middle).Time;
while (this.HitTest(dateAreaLeft + dayCellWidth, middle).Time == dayCellTime) {
dayCellWidth++;
}
// Record day box size and actual first day of the month used
this.calendarFirstDayOfWeek = dayCellTime.DayOfWeek;
this.dayCellWidth = dayCellWidth;
this.dayCellHeight = dayCellHeight;
}
}
My testing shows that Windows 7 does not have the painting problem and I expect that neither does Vista, so this only attempts a fix for Windows XP.
you can try this code:
Dim StartDate As Date = New DateTime(2011, 9, 21)
Dim EndDate As Date = New DateTime(2011, 9, 25)
MonthCalendar1.SelectionRange = New SelectionRange(StartDate, EndDate)
for more information:
http://www.authorcode.com/how-to-select-a-range-of-dates-in-the-monthcalendar-control/
http://www.authorcode.com/how-to-enable-windows-xp-visual-styles-of-net-application/
Related
How would I go about finding out how a Windows Forms Control was made? I want to create a Control from scratch. Preferably a ListBox or even better, a ListView Control, but I have no idea where to start.
Some suggestions I've come across in the past have been:
Use a Panel Control and dynamically add Label controls to it with appropriate styling, and;
Extend or Subclass the ListView/ListBox Controls, and set OwnerDraw to true, and do your custom drawing in the OnPaint event.
But I want more control than that. I don't just want a ListView Control, I don't want to use a third-party control either (no matter how good [Object ListView] is1. I want my own ListView Control. I don't care how hard it is, but is this possible in Windows Forms? Where should I start?
Would I need to use GDI/GDI+ to draw everything? Would I start with an empty Panel Control and then manually draw each List Item using the System.Drawing namespace?
How would I go about finding out how a Windows Forms Control was made?
Simple, Every control is a Window created using CreateWindowEx method (done internally by the Winforms).
In winforms point of view: Control is the base class for all Windows. There are some controls which has been written in unmanaged code like ListView, ListBox etc. For them you can't see the paint code in .net. It is implemented in OS itself(not sure which dll they live). Winforms just provides a wrapper over those unmanaged controls.
But, there are purely managed controls written in c#. Example: DataGridView. You can go through the code. Here master is OnPaint protected method. That is the place where you need to write all your custom painting logic with the Graphics instance provided.
Key is you'll create a "Datastructure" which holds all the necessary items to draw your control. Lets say ItemRectangle, Text, Color, Font, etc.. Then you use them all together to paint your custom control in OnPaint method.
Would I need to use GDI/GDI+ to draw everything?
You'll use System.Drawing and System.Drawing.Drawing2D namespaces to draw your control. If something that .net doesn't provide you'll p/invoke Gdi/Gdi+
Advice for choosing base class: If your control needs to be scrollable(ListView kind of controls likely need it). So you can choose ScrollableControl or Panel as base class which supports scrolling. Otherwise you can inherit from Control class.
Developing Custom Windows Forms Controls with the .NET Framework
All the best :)
You can inherit Usercontrol and start writing from scratch or if you want specific functionalities like listview you can inherit the relevant control.
Just googling "custom listview control" or "custom control (yourtype) will give 1000's of results".
Hope this helps
eg code for Vista Style button:
public class VistaButton : System.Windows.Forms.UserControl
{
#region - Designer -
private System.ComponentModel.Container components = null;
/// <summary>
/// Initialize the component with it's
/// default settings.
/// </summary>
public VistaButton()
{
InitializeComponent();
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer, true);
this.SetStyle(ControlStyles.ResizeRedraw, true);
this.SetStyle(ControlStyles.Selectable, true);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.SetStyle(ControlStyles.UserPaint, true);
this.BackColor = Color.Transparent;
mFadeIn.Interval = 30;
mFadeOut.Interval = 30;
}
/// <summary>
/// Release resources used by the control.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region - Component Designer generated code -
private void InitializeComponent()
{
//
// VistaButton
//
this.Name = "VistaButton";
this.Size = new System.Drawing.Size(100, 32);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.VistaButton_Paint);
this.KeyUp += new System.Windows.Forms.KeyEventHandler(this.VistaButton_KeyUp);
this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.VistaButton_KeyDown);
this.MouseEnter += new System.EventHandler(this.VistaButton_MouseEnter);
this.MouseLeave += new System.EventHandler(this.VistaButton_MouseLeave);
this.MouseUp +=new MouseEventHandler(VistaButton_MouseUp);
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.VistaButton_MouseDown);
this.GotFocus +=new EventHandler(VistaButton_MouseEnter);
this.LostFocus +=new EventHandler(VistaButton_MouseLeave);
this.mFadeIn.Tick += new EventHandler(mFadeIn_Tick);
this.mFadeOut.Tick += new EventHandler(mFadeOut_Tick);
this.Resize +=new EventHandler(VistaButton_Resize);
}
#endregion
#endregion
#region - Enums -
/// <summary>
/// A private enumeration that determines
/// the mouse state in relation to the
/// current instance of the control.
/// </summary>
enum State {None, Hover, Pressed};
/// <summary>
/// A public enumeration that determines whether
/// the button background is painted when the
/// mouse is not inside the ClientArea.
/// </summary>
public enum Style
{
/// <summary>
/// Draw the button as normal
/// </summary>
Default,
/// <summary>
/// Only draw the background on mouse over.
/// </summary>
Flat
};
#endregion
#region - Properties -
#region - Private Variables -
private bool calledbykey = false;
private State mButtonState = State.None;
private Timer mFadeIn = new Timer();
private Timer mFadeOut = new Timer();
private int mGlowAlpha = 0;
#endregion
#region - Text -
private string mText;
/// <summary>
/// The text that is displayed on the button.
/// </summary>
[Category("Text"),
Description("The text that is displayed on the button.")]
public string ButtonText
{
get { return mText; }
set { mText = value; this.Invalidate(); }
}
private Color mForeColor = Color.White;
/// <summary>
/// The color with which the text is drawn.
/// </summary>
[Category("Text"),
Browsable(true),
DefaultValue(typeof(Color),"White"),
Description("The color with which the text is drawn.")]
public override Color ForeColor
{
get { return mForeColor; }
set { mForeColor = value; this.Invalidate(); }
}
private ContentAlignment mTextAlign = ContentAlignment.MiddleCenter;
/// <summary>
/// The alignment of the button text
/// that is displayed on the control.
/// </summary>
[Category("Text"),
DefaultValue(typeof(ContentAlignment),"MiddleCenter"),
Description("The alignment of the button text " +
"that is displayed on the control.")]
public ContentAlignment TextAlign
{
get { return mTextAlign; }
set { mTextAlign = value; this.Invalidate(); }
}
#endregion
#region - Image -
private Image mImage;
/// <summary>
/// The image displayed on the button that
/// is used to help the user identify
/// it's function if the text is ambiguous.
/// </summary>
[Category("Image"),
DefaultValue(null),
Description("The image displayed on the button that " +
"is used to help the user identify" +
"it's function if the text is ambiguous.")]
public Image Image
{
get { return mImage; }
set { mImage = value; this.Invalidate(); }
}
private ContentAlignment mImageAlign = ContentAlignment.MiddleLeft;
/// <summary>
/// The alignment of the image
/// in relation to the button.
/// </summary>
[Category("Image"),
DefaultValue(typeof(ContentAlignment),"MiddleLeft"),
Description("The alignment of the image " +
"in relation to the button.")]
public ContentAlignment ImageAlign
{
get { return mImageAlign; }
set { mImageAlign = value; this.Invalidate(); }
}
private Size mImageSize = new Size(24,24);
/// <summary>
/// The size of the image to be displayed on the
/// button. This property defaults to 24x24.
/// </summary>
[Category("Image"),
DefaultValue(typeof(Size),"24, 24"),
Description("The size of the image to be displayed on the" +
"button. This property defaults to 24x24.")]
public Size ImageSize
{
get { return mImageSize; }
set { mImageSize = value; this.Invalidate(); }
}
#endregion
#region - Appearance -
private Style mButtonStyle = Style.Default;
/// <summary>
/// Sets whether the button background is drawn
/// while the mouse is outside of the client area.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Style),"Default"),
Description("Sets whether the button background is drawn " +
"while the mouse is outside of the client area.")]
public Style ButtonStyle
{
get { return mButtonStyle; }
set { mButtonStyle = value; this.Invalidate(); }
}
private int mCornerRadius = 8;
/// <summary>
/// The radius for the button corners. The
/// greater this value is, the more 'smooth'
/// the corners are. This property should
/// not be greater than half of the
/// controls height.
/// </summary>
[Category("Appearance"),
DefaultValue(8),
Description("The radius for the button corners. The " +
"greater this value is, the more 'smooth' " +
"the corners are. This property should " +
"not be greater than half of the " +
"controls height.")]
public int CornerRadius
{
get { return mCornerRadius; }
set { mCornerRadius = value; this.Invalidate(); }
}
private Color mHighlightColor = Color.White;
/// <summary>
/// The colour of the highlight on the top of the button.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "White"),
Description("The colour of the highlight on the top of the button.")]
public Color HighlightColor
{
get { return mHighlightColor; }
set { mHighlightColor = value; this.Invalidate(); }
}
private Color mButtonColor = Color.Black;
/// <summary>
/// The bottom color of the button that
/// will be drawn over the base color.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "Black"),
Description("The bottom color of the button that " +
"will be drawn over the base color.")]
public Color ButtonColor
{
get { return mButtonColor; }
set { mButtonColor = value; this.Invalidate(); }
}
private Color mGlowColor = Color.FromArgb(141,189,255);
/// <summary>
/// The colour that the button glows when
/// the mouse is inside the client area.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "141,189,255"),
Description("The colour that the button glows when " +
"the mouse is inside the client area.")]
public Color GlowColor
{
get { return mGlowColor; }
set { mGlowColor = value; this.Invalidate(); }
}
private Image mBackImage;
/// <summary>
/// The background image for the button,
/// this image is drawn over the base
/// color of the button.
/// </summary>
[Category("Appearance"),
DefaultValue(null),
Description("The background image for the button, " +
"this image is drawn over the base " +
"color of the button.")]
public Image BackImage
{
get { return mBackImage; }
set { mBackImage = value; this.Invalidate(); }
}
private Color mBaseColor = Color.Black;
/// <summary>
/// The backing color that the rest of
/// the button is drawn. For a glassier
/// effect set this property to Transparent.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "Black"),
Description("The backing color that the rest of" +
"the button is drawn. For a glassier " +
"effect set this property to Transparent.")]
public Color BaseColor
{
get { return mBaseColor; }
set { mBaseColor = value; this.Invalidate(); }
}
#endregion
#region - Behaviour -
private DialogResult mDialogResult = DialogResult.OK;
/// <summary>
/// Specify the dialog result property.
/// </summary>
[Category("Behaviour"),
DefaultValue(typeof(DialogResult)),
Description("The Dialog-Box result produced in a modal form" +
"by clicking the button.")]
public virtual DialogResult DialogResult
{
get { return mDialogResult; }
set { mDialogResult = value; this.Invalidate(); }
}
#endregion
#endregion
#region - Functions -
private GraphicsPath RoundRect(RectangleF r, float r1, float r2, float r3, float r4)
{
float x = r.X, y = r.Y, w = r.Width, h = r.Height;
GraphicsPath rr = new GraphicsPath();
rr.AddBezier(x, y + r1, x, y, x + r1, y, x + r1, y);
rr.AddLine(x + r1, y, x + w - r2, y);
rr.AddBezier(x + w - r2, y, x + w, y, x + w, y + r2, x + w, y + r2);
rr.AddLine(x + w, y + r2, x + w, y + h - r3);
rr.AddBezier(x + w, y + h - r3, x + w, y + h, x + w - r3, y + h, x + w - r3, y + h);
rr.AddLine(x + w - r3, y + h, x + r4, y + h);
rr.AddBezier(x + r4, y + h, x, y + h, x, y + h - r4, x, y + h - r4);
rr.AddLine(x, y + h - r4, x, y + r1);
return rr;
}
private StringFormat StringFormatAlignment(ContentAlignment textalign)
{
StringFormat sf = new StringFormat();
switch (textalign)
{
case ContentAlignment.TopLeft:
case ContentAlignment.TopCenter:
case ContentAlignment.TopRight:
sf.LineAlignment = StringAlignment.Near;
break;
case ContentAlignment.MiddleLeft:
case ContentAlignment.MiddleCenter:
case ContentAlignment.MiddleRight:
sf.LineAlignment = StringAlignment.Center;
break;
case ContentAlignment.BottomLeft:
case ContentAlignment.BottomCenter:
case ContentAlignment.BottomRight:
sf.LineAlignment = StringAlignment.Far;
break;
}
switch (textalign)
{
case ContentAlignment.TopLeft:
case ContentAlignment.MiddleLeft:
case ContentAlignment.BottomLeft:
sf.Alignment = StringAlignment.Near;
break;
case ContentAlignment.TopCenter:
case ContentAlignment.MiddleCenter:
case ContentAlignment.BottomCenter:
sf.Alignment = StringAlignment.Center;
break;
case ContentAlignment.TopRight:
case ContentAlignment.MiddleRight:
case ContentAlignment.BottomRight:
sf.Alignment = StringAlignment.Far;
break;
}
return sf;
}
#endregion
#region - Drawing -
/// <summary>
/// Draws the outer border for the control
/// using the ButtonColor property.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawOuterStroke(Graphics g)
{
Color buttonColor = this.ButtonColor;
if (!this.Enabled)
buttonColor = System.Drawing.SystemColors.ControlDark;
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
Rectangle r = this.ClientRectangle;
r.Width -= 1; r.Height -= 1;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
using (Pen p = new Pen(buttonColor))
{
g.DrawPath(p, rr);
}
}
}
/// <summary>
/// Draws the inner border for the control
/// using the HighlightColor property.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawInnerStroke(Graphics g)
{
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
Rectangle r = this.ClientRectangle;
r.X++; r.Y++;
r.Width -= 3; r.Height -= 3;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
using (Pen p = new Pen(this.HighlightColor))
{
g.DrawPath(p, rr);
}
}
}
/// <summary>
/// Draws the background for the control
/// using the background image and the
/// BaseColor.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawBackground(Graphics g)
{
Color baseColor = this.BaseColor;
Color buttonColor = this.ButtonColor;
if (!this.Enabled)
{
baseColor = SystemColors.Control;
buttonColor = System.Drawing.SystemColors.Control;
}
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
int alpha = (mButtonState == State.Pressed) ? 204 : 127;
Rectangle r = this.ClientRectangle;
r.Width--; r.Height--;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
using (SolidBrush sb = new SolidBrush(baseColor))
{
g.FillPath(sb, rr);
}
SetClip(g);
if (this.BackImage != null){g.DrawImage(this.BackImage, this.ClientRectangle);}
g.ResetClip();
using (SolidBrush sb = new SolidBrush(Color.FromArgb(alpha, buttonColor)))
{
g.FillPath(sb, rr);
}
}
}
/// <summary>
/// Draws the Highlight over the top of the
/// control using the HightlightColor.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawHighlight(Graphics g)
{
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
int alpha = (mButtonState == State.Pressed) ? 60 : 150;
Rectangle rect = new Rectangle(0, 0, this.Width, this.Height / 2);
using (GraphicsPath r = RoundRect(rect, CornerRadius, CornerRadius, 0, 0))
{
using (LinearGradientBrush lg = new LinearGradientBrush(r.GetBounds(),
Color.FromArgb(alpha, this.HighlightColor),
Color.FromArgb(alpha / 3, this.HighlightColor),
LinearGradientMode.Vertical))
{
g.FillPath(lg, r);
}
}
}
/// <summary>
/// Draws the glow for the button when the
/// mouse is inside the client area using
/// the GlowColor property.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawGlow(Graphics g)
{
if (this.mButtonState == State.Pressed){return;}
SetClip(g);
using (GraphicsPath glow = new GraphicsPath())
{
glow.AddEllipse(-5,this.Height / 2 - 10, this.Width + 11, this.Height + 11);
using (PathGradientBrush gl = new PathGradientBrush(glow))
{
gl.CenterColor = Color.FromArgb(mGlowAlpha, this.GlowColor);
gl.SurroundColors = new Color[] {Color.FromArgb(0, this.GlowColor)};
g.FillPath(gl, glow);
}
}
g.ResetClip();
}
/// <summary>
/// Draws the text for the button.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawText(Graphics g)
{
Color foreColor = this.ForeColor;
if (!this.Enabled)
foreColor = System.Drawing.SystemColors.ControlDark;
StringFormat sf = StringFormatAlignment(this.TextAlign);
Rectangle r = new Rectangle(8,8,this.Width - 17,this.Height - 17);
g.DrawString(this.ButtonText, this.Font, new SolidBrush(foreColor), r, sf);
}
/// <summary>
/// Draws the image for the button
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawImage(Graphics g)
{
if (this.Image == null) {return;}
Rectangle r = new Rectangle(8,8,this.ImageSize.Width,this.ImageSize.Height);
switch (this.ImageAlign)
{
case ContentAlignment.TopCenter:
r = new Rectangle(this.Width / 2 - this.ImageSize.Width / 2,8,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.TopRight:
r = new Rectangle(this.Width - 8 - this.ImageSize.Width,8,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.MiddleLeft:
r = new Rectangle(8,this.Height / 2 - this.ImageSize.Height / 2,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.MiddleCenter:
r = new Rectangle(this.Width / 2 - this.ImageSize.Width / 2,this.Height / 2 - this.ImageSize.Height / 2,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.MiddleRight:
r = new Rectangle(this.Width - 8 - this.ImageSize.Width,this.Height / 2 - this.ImageSize.Height / 2,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.BottomLeft:
r = new Rectangle(8,this.Height - 8 - this.ImageSize.Height,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.BottomCenter:
r = new Rectangle(this.Width / 2 - this.ImageSize.Width / 2,this.Height - 8 - this.ImageSize.Height,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.BottomRight:
r = new Rectangle(this.Width - 8 - this.ImageSize.Width,this.Height - 8 - this.ImageSize.Height,this.ImageSize.Width,this.ImageSize.Height);
break;
}
g.DrawImage(this.Image,r);
}
private void SetClip(Graphics g)
{
Rectangle r = this.ClientRectangle;
r.X++; r.Y++; r.Width-=3; r.Height-=3;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
g.SetClip(rr);
}
}
#endregion
#region - Private Subs -
private void VistaButton_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
DrawBackground(e.Graphics);
DrawHighlight(e.Graphics);
DrawImage(e.Graphics);
DrawText(e.Graphics);
DrawGlow(e.Graphics);
DrawOuterStroke(e.Graphics);
DrawInnerStroke(e.Graphics);
}
private void VistaButton_Resize(object sender, EventArgs e)
{
Rectangle r = this.ClientRectangle;
r.X -= 1; r.Y -= 1;
r.Width += 2; r.Height += 2;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
this.Region = new Region(rr);
}
}
#region - Mouse and Keyboard Events -
private void VistaButton_MouseEnter(object sender, EventArgs e)
{
mButtonState = State.Hover;
mFadeOut.Stop();
mFadeIn.Start();
}
private void VistaButton_MouseLeave(object sender, EventArgs e)
{
mButtonState = State.None;
if (this.mButtonStyle == Style.Flat) { mGlowAlpha = 0; }
mFadeIn.Stop();
mFadeOut.Start();
}
private void VistaButton_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
mButtonState = State.Pressed;
if (this.mButtonStyle != Style.Flat) { mGlowAlpha = 255; }
mFadeIn.Stop();
mFadeOut.Stop();
this.Invalidate();
}
}
private void mFadeIn_Tick(object sender, EventArgs e)
{
if (this.ButtonStyle == Style.Flat) {mGlowAlpha = 0;}
if (mGlowAlpha + 30 >= 255)
{
mGlowAlpha = 255;
mFadeIn.Stop();
}
else
{
mGlowAlpha += 30;
}
this.Invalidate();
}
private void mFadeOut_Tick(object sender, EventArgs e)
{
if (this.ButtonStyle == Style.Flat) {mGlowAlpha = 0;}
if (mGlowAlpha - 30 <= 0)
{
mGlowAlpha = 0;
mFadeOut.Stop();
}
else
{
mGlowAlpha -= 30;
}
this.Invalidate();
}
private void VistaButton_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
MouseEventArgs m = new MouseEventArgs(MouseButtons.Left,0,0,0,0);
VistaButton_MouseDown(sender, m);
}
}
private void VistaButton_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
MouseEventArgs m = new MouseEventArgs(MouseButtons.Left,0,0,0,0);
calledbykey = true;
VistaButton_MouseUp(sender, m);
}
}
private void VistaButton_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
mButtonState = State.Hover;
mFadeIn.Stop();
mFadeOut.Stop();
this.Invalidate();
if (calledbykey == true) {this.OnClick(EventArgs.Empty); calledbykey = false;}
}
}
#endregion
#endregion
}
This control is use to scroll text.
I want to color each line of the text in a different color.
The text is written in a textBox and the property TextToScroll get the text from the textBox.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace ExtendedComponents
{
public partial class Scroller : UserControl
{
/// <summary>
/// String list.
/// </summary>
string[] m_text = new string[0];
/// <summary>
/// Offset for animation.
/// </summary>
int m_scrollingOffset = 0;
/// <summary>
/// Top part size of text in percents.
/// </summary>
int m_topPartSizePercent = 50;
/// <summary>
/// Font, which is used to draw.
/// </summary>
Font m_font = new Font("Arial", 20, FontStyle.Bold, GraphicsUnit.Pixel);
/// <summary>
/// Constructor
/// </summary>
public Scroller()
{
InitializeComponent();
// Enables double buffering (to remove flickering) and enables user paint.
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
}
/// <summary>
/// Text to scroll.
/// </summary>
public string TextToScroll
{
get
{
return string.Join("\n", m_text);
}
set
{
string buffer = value;
// Splits text by "\n" symbol.
m_text = buffer.Split(new char[1] { '\n' });
}
}
/// <summary>
/// Timer interval.
/// </summary>
public int Interval
{
get
{
return m_timer.Interval;
}
set
{
m_timer.Interval = value;
}
}
/// <summary>
/// Font, which is used to draw.
/// </summary>
public Font TextFont
{
get
{
return m_font;
}
set
{
m_font = value;
}
}
/// <summary>
/// Top part size of text in percents (of control width).
/// </summary>
public int TopPartSizePercent
{
get
{
return m_topPartSizePercent;
}
set
{
if ((value >= 10) && (value <= 100))
{
m_topPartSizePercent = value;
}
else
throw new InvalidEnumArgumentException("The value must be more than zero. and less than 100.");
}
}
/// <summary>
/// Paint handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnPaint(object sender, PaintEventArgs e)
{
// Sets antialiasing mode for better quality.
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
// Prepares background.
e.Graphics.FillRectangle(new SolidBrush(this.BackColor), this.ClientRectangle);
// Creates GraphicsPath for text.
GraphicsPath path = new GraphicsPath();
// Visible lines counter;
int visibleLines = 0;
for (int i = m_text.Length - 1; i >= 0; i--)
{
Point pt = new Point((int)((this.ClientSize.Width - e.Graphics.MeasureString(m_text[i], m_font).Width) / 2),
(int)(m_scrollingOffset + this.ClientSize.Height - (m_text.Length - i) * m_font.Size));
// Adds visible lines to path.
if ((pt.Y + this.Font.Size > 0) && (pt.Y < this.Height))
{
path.AddString(m_text[i], m_font.FontFamily, (int)m_font.Style, m_font.Size,
pt, StringFormat.GenericTypographic);
visibleLines++;
}
String drawString = m_text[i];
Font drawFont = new Font("Arial", 16);
SolidBrush drawBrush = new SolidBrush(Color.Red);
e.Graphics.DrawString(drawString, drawFont, drawBrush, pt);
}
// For repeat scrolling.
if ((visibleLines == 0) && (m_scrollingOffset < 0))
{
m_scrollingOffset = (int)this.Font.SizeInPoints * m_text.Length;
}
int topSizeWidth = (int)(this.Width * m_topPartSizePercent / 100.0f);
// Wraps Graphics path from rectangle to trapeze.
path.Warp(
new PointF[4]
{
new PointF((this.Width - topSizeWidth) / 2, 0),
new PointF(this.Width - (this.Width - topSizeWidth) / 2, 0),
new PointF(0, this.Height),
new PointF(this.Width, this.Height)
},
new RectangleF(this.ClientRectangle.X, this.ClientRectangle.Y, this.ClientRectangle.Width, this.ClientRectangle.Height),
null,
WarpMode.Perspective
);
// Draws wrapped path.
e.Graphics.FillPath(new SolidBrush(this.ForeColor), path);
path.Dispose();
// Draws fog effect with help of gradient brush with alpha colors.
/*using (Brush br = new LinearGradientBrush(new Point(0, 0), new Point(0, this.Height),
Color.FromArgb(255, this.BackColor), Color.FromArgb(0, this.BackColor)))
{
e.Graphics.FillRectangle(br, this.ClientRectangle);
}*/
}
/// <summary>
/// Starts the animation from the beginning.
/// </summary>
public void Start()
{
// Calculates scrolling offset.
m_scrollingOffset = (int)this.Font.SizeInPoints * m_text.Length;
m_timer.Start();
}
/// <summary>
/// Stops the animation.
/// </summary>
public void Stop()
{
m_timer.Stop();
}
/// <summary>
/// Timer handler.
/// </summary>
private void OnTimerTick(object sender, EventArgs e)
{
// Changes the offset.
m_scrollingOffset--;
// Repaints whole control area.
Invalidate();
}
}
}
I added this part in the onpaint event but this will paint the whole text in Red.
How can i paint each line in the text in a different color ?
String drawString = m_text[i];
Font drawFont = new Font("Arial", 16);
SolidBrush drawBrush = new SolidBrush(Color.Red);
e.Graphics.DrawString(drawString, drawFont, drawBrush, pt);
You have to draw each line separately. Use MeasureString to help you figure out the padding between each line.
http://msdn.microsoft.com/en-us/library/system.drawing.graphics.measurestring(v=vs.110).aspx
Generate color randomly and pass it to SolidBrush
Random r = new Random();
for (int i = m_text.Length - 1; i >= 0; i--)
{
Point pt = new Point((int)((this.ClientSize.Width - e.Graphics.MeasureString(m_text[i], m_font).Width) / 2),
(int)(m_scrollingOffset + this.ClientSize.Height - (m_text.Length - i) * m_font.Size));
// Adds visible lines to path.
if ((pt.Y + this.Font.Size > 0) && (pt.Y < this.Height))
{
path.AddString(m_text[i], m_font.FontFamily, (int)m_font.Style, m_font.Size,
pt, StringFormat.GenericTypographic);
visibleLines++;
}
Color c = Color.FromArgb(r.Next(0, 255), r.Next(0, 255), r.Next(0, 255));
Font drawFont = new Font("Arial", 16);
e.Graphics.DrawString( m_text[i], drawFont, new SolidBrush(c), pt);
}
I'm implementing pinch zoom behavior on image bound inside data template. I found the solution here: Pinch Zoom images bound in Listbox, Here's my sample code on xaml:
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Image delay:LowProfileImageLoader.UriSource="{Binding hires}"
CacheMode="BitmapCache"
Grid.Column="0"
Width="450"
Height="750"
VerticalAlignment="Center"
HorizontalAlignment="Center" Stretch="UniformToFill">
<i:Interaction.Behaviors>
<Behaviors:PinchZomBehavior/>
</i:Interaction.Behaviors>
</Image>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
Here's the code of PinchZomBehavior.cs
public class PinchZomBehavior : Behavior<Image>
{
private double _totalImageScale = 1d;
private Point _imagePosition = new Point(0, 0);
private const double MaxImageZoom = 5;
private Point _oldFinger1;
private Point _oldFinger2;
private double _oldScaleFactor;
private Image _imgZoom;
protected override void OnAttached()
{
_imgZoom = AssociatedObject;
_imgZoom.RenderTransform = new CompositeTransform { ScaleX = 1, ScaleY = 1, TranslateX = 0, TranslateY = 0 };
var listener = GestureService.GetGestureListener(AssociatedObject);
listener.PinchStarted += OnPinchStarted;
listener.PinchDelta += OnPinchDelta;
listener.DragDelta += OnDragDelta;
listener.DoubleTap += OnDoubleTap;
base.OnAttached();
}
#region Pinch and Zoom Logic
#region Event handlers
/// <summary>
/// Initializes the zooming operation
/// </summary>
private void OnPinchStarted(object sender, PinchStartedGestureEventArgs e)
{
_oldFinger1 = e.GetPosition(_imgZoom, 0);
_oldFinger2 = e.GetPosition(_imgZoom, 1);
_oldScaleFactor = 1;
}
/// <summary>
/// Computes the scaling and translation to correctly zoom around your fingers.
/// </summary>
private void OnPinchDelta(object sender, PinchGestureEventArgs e)
{
var scaleFactor = e.DistanceRatio / _oldScaleFactor;
if (!IsScaleValid(scaleFactor))
return;
var currentFinger1 = e.GetPosition(_imgZoom, 0);
var currentFinger2 = e.GetPosition(_imgZoom, 1);
var translationDelta = GetTranslationDelta(
currentFinger1,
currentFinger2,
_oldFinger1,
_oldFinger2,
_imagePosition,
scaleFactor);
_oldFinger1 = currentFinger1;
_oldFinger2 = currentFinger2;
_oldScaleFactor = e.DistanceRatio;
UpdateImageScale(scaleFactor);
UpdateImagePosition(translationDelta);
}
/// <summary>
/// Moves the image around following your finger.
/// </summary>
private void OnDragDelta(object sender, DragDeltaGestureEventArgs e)
{
var translationDelta = new Point(e.HorizontalChange, e.VerticalChange);
if (IsDragValid(1, translationDelta))
UpdateImagePosition(translationDelta);
}
/// <summary>
/// Resets the image scaling and position
/// </summary>
private void OnDoubleTap(object sender, Microsoft.Phone.Controls.GestureEventArgs e)
{
ResetImagePosition();
}
#endregion
#region Utils
/// <summary>
/// Computes the translation needed to keep the image centered between your fingers.
/// </summary>
private Point GetTranslationDelta(
Point currentFinger1, Point currentFinger2,
Point oldFinger1, Point oldFinger2,
Point currentPosition, double scaleFactor)
{
var newPos1 = new Point(
currentFinger1.X + (currentPosition.X - oldFinger1.X) * scaleFactor,
currentFinger1.Y + (currentPosition.Y - oldFinger1.Y) * scaleFactor);
var newPos2 = new Point(
currentFinger2.X + (currentPosition.X - oldFinger2.X) * scaleFactor,
currentFinger2.Y + (currentPosition.Y - oldFinger2.Y) * scaleFactor);
var newPos = new Point(
(newPos1.X + newPos2.X) / 2,
(newPos1.Y + newPos2.Y) / 2);
return new Point(
newPos.X - currentPosition.X,
newPos.Y - currentPosition.Y);
}
/// <summary>
/// Updates the scaling factor by multiplying the delta.
/// </summary>
private void UpdateImageScale(double scaleFactor)
{
_totalImageScale *= scaleFactor;
ApplyScale();
}
/// <summary>
/// Applies the computed scale to the image control.
/// </summary>
private void ApplyScale()
{
((CompositeTransform)_imgZoom.RenderTransform).ScaleX = _totalImageScale;
((CompositeTransform)_imgZoom.RenderTransform).ScaleY = _totalImageScale;
}
/// <summary>
/// Updates the image position by applying the delta.
/// Checks that the image does not leave empty space around its edges.
/// </summary>
private void UpdateImagePosition(Point delta)
{
var newPosition = new Point(_imagePosition.X + delta.X, _imagePosition.Y + delta.Y);
if (newPosition.X > 0) newPosition.X = 0;
if (newPosition.Y > 0) newPosition.Y = 0;
if ((_imgZoom.ActualWidth * _totalImageScale) + newPosition.X < _imgZoom.ActualWidth)
newPosition.X = _imgZoom.ActualWidth - (_imgZoom.ActualWidth * _totalImageScale);
if ((_imgZoom.ActualHeight * _totalImageScale) + newPosition.Y < _imgZoom.ActualHeight)
newPosition.Y = _imgZoom.ActualHeight - (_imgZoom.ActualHeight * _totalImageScale);
_imagePosition = newPosition;
ApplyPosition();
}
/// <summary>
/// Applies the computed position to the image control.
/// </summary>
private void ApplyPosition()
{
((CompositeTransform)_imgZoom.RenderTransform).TranslateX = _imagePosition.X;
((CompositeTransform)_imgZoom.RenderTransform).TranslateY = _imagePosition.Y;
}
/// <summary>
/// Resets the zoom to its original scale and position
/// </summary>
private void ResetImagePosition()
{
_totalImageScale = 1;
_imagePosition = new Point(0, 0);
ApplyScale();
ApplyPosition();
}
/// <summary>
/// Checks that dragging by the given amount won't result in empty space around the image
/// </summary>
private bool IsDragValid(double scaleDelta, Point translateDelta)
{
if (_imagePosition.X + translateDelta.X > 0 || _imagePosition.Y + translateDelta.Y > 0)
return false;
if ((_imgZoom.ActualWidth * _totalImageScale * scaleDelta) + (_imagePosition.X + translateDelta.X) < _imgZoom.ActualWidth)
return false;
if ((_imgZoom.ActualHeight * _totalImageScale * scaleDelta) + (_imagePosition.Y + translateDelta.Y) < _imgZoom.ActualHeight)
return false;
return true;
}
/// <summary>
/// Tells if the scaling is inside the desired range
/// </summary>
private bool IsScaleValid(double scaleDelta)
{
return (_totalImageScale * scaleDelta >= 1) && (_totalImageScale * scaleDelta <= MaxImageZoom);
}
#endregion
#endregion
}
I already included the using tags and the:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
But I still got the error on xaml that says:
The type 'Behaviors:PinchZomBehavior' was not found. Verify that you are not missing an assembly reference and that all referenced assemblies have been built.
Hope you can help me with this. I need to finish my project on time. thanks in advance!
Did you forget to add the namespace for the behavior itself? Something like:
xmlns:Behaviors="clr-namespace:NAMESPACE;assembly=ASSEMBLY"
I cannot guess the namespace and the assembly name, you'll have to provide it yourself.
Before anybody points it out I know that a there is a question with the same title that has already been asked here it just doesn't answer my issue I think.
Working in .NET 3.5 As in that question I am making an area selection component to select an area on a picture. The picture is displayed using a custom control in which the picture is drawn during OnPaint.
I have the following code for my selection rectangle:
internal class AreaSelection : Control
{
private Rectangle selection
{
get { return new Rectangle(Point.Empty, Size.Subtract(this.Size, new Size(1, 1))); }
}
private Size mouseStartLocation;
public AreaSelection()
{
this.Size = new Size(150, 150);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.FromArgb(70, 200, 200, 200);
}
protected override void OnMouseEnter(EventArgs e)
{
this.Cursor = Cursors.SizeAll;
base.OnMouseEnter(e);
}
protected override void OnMouseDown(MouseEventArgs e)
{
this.mouseStartLocation = new Size(e.Location);
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Point offset = e.Location - this.mouseStartLocation;
this.Left += offset.X;
this.Top += offset.Y;
}
base.OnMouseMove(e);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawRectangle(new Pen(Color.Black) { DashStyle = DashStyle.Dash }, this.selection);
Debug.WriteLine("Selection redrawn");
}
}
Which gives me a nice semi-transparent rectangle which I can drag around. The problem I have is that whilst dragging the underlying image which shows through the rectangle gets lags behind the position of the rectangle.
This gets more noticeable the faster I move the rectangle. When I stop moving it the image catches up and everything aligns perfectly again.
I assume that there is something wrong with the way the rectangle draws, but I really can't figure out what it is...
Any help would be much appreciated.
EDIT:
I have noticed that the viewer gets redrawn twice as often as the selection area when I drag the selection area. Could this be the cause of the problem?
EDIT 2:
Here is the code for the viewer in case it is relevant:
public enum ImageViewerViewMode
{
Normal,
PrintSelection,
PrintPreview
}
public enum ImageViewerZoomMode
{
None,
OnClick,
Lens
}
public partial class ImageViewer : UserControl
{
/// <summary>
/// The current zoom factor. Note: Use SetZoom() to set the value.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float ZoomFactor
{
get { return this.zoomFactor; }
private set
{
this.zoomFactor = value;
}
}
/// <summary>
/// The maximum zoom factor to use
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float MaximumZoomFactor
{
get
{
return this.maximumZoomFactor;
}
set
{
this.maximumZoomFactor = value;
this.SetZoomFactorLimits();
}
}
/// <summary>
/// The minimum zoom factort to use
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float MinimumZoomFactor
{
get
{
return this.minimumZoomFactor;
}
set
{
this.minimumZoomFactor = value;
this.SetZoomFactorLimits();
}
}
/// <summary>
/// The multiplying factor to apply to each ZoomIn/ZoomOut command
/// </summary>
[Category("Behavior")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[DefaultValue(2F)]
public float ZoomStep { get; set; }
/// <summary>
/// The image currently displayed by the control
/// </summary>
[Category("Data")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public Image Image
{
get { return this.image; }
set
{
this.image = value;
this.ZoomExtents();
this.minimumZoomFactor = this.zoomFactor / 10;
this.MaximumZoomFactor = this.zoomFactor * 10;
}
}
public ImageViewerViewMode ViewMode { get; set; }
public ImageViewerZoomMode ZoomMode { get; set; }
private ImageViewerLens Lens { get; set; }
private float zoomFactor;
private float minimumZoomFactor;
private float maximumZoomFactor;
private bool panning;
private Point imageLocation;
private Point imageTranslation;
private Image image;
private AreaSelection areaSelection;
/// <summary>
/// Class constructor
/// </summary>
public ImageViewer()
{
this.DoubleBuffered = true;
this.MinimumZoomFactor = 0.1F;
this.MaximumZoomFactor = 10F;
this.ZoomStep = 2F;
this.UseScannerUI = true;
this.Lens = new ImageViewerLens();
this.ViewMode = ImageViewerViewMode.PrintSelection;
this.areaSelection = new AreaSelection();
this.Controls.Add(this.areaSelection);
// TWAIN
// Initialise twain
this.twain = new Twain(new WinFormsWindowMessageHook(this));
// Try to set the last used default scanner
if (this.AvailableScanners.Any())
{
this.twain.TransferImage += twain_TransferImage;
this.twain.ScanningComplete += twain_ScanningComplete;
if (!this.SetScanner(this.defaultScanner))
this.SetScanner(this.AvailableScanners.First());
}
}
/// <summary>
/// Saves the currently loaded image under the specified filename, in the specified format at the specified quality
/// </summary>
/// <param name="FileName">The file name (full file path) under which to save the file. File type extension is not required.</param>
/// <param name="Format">The file format under which to save the file</param>
/// <param name="Quality">The quality in percent of the image to save. This is optional and may or may not be used have an effect depending on the chosen file type. Default is maximum quality.</param>
public void SaveImage(string FileName, GraphicFormats Format, uint Quality = 100)
{
ImageCodecInfo encoder;
EncoderParameters encoderParameters;
if (FileName.IsNullOrEmpty())
throw new ArgumentNullException(FileName);
else
{
string extension = Path.GetExtension(FileName);
if (!string.IsNullOrEmpty(extension))
FileName = FileName.Replace(extension, string.Empty);
FileName += "." + Format.ToString();
}
Quality = Math.Min(Math.Max(1, Quality), 100);
if (!TryGetEncoder(Format, out encoder))
return;
encoderParameters = new EncoderParameters(1);
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, (int)Quality);
this.Image.Save(FileName, encoder, encoderParameters);
}
/// <summary>
/// Tries to retrieve the appropriate encoder for the chose image format.
/// </summary>
/// <param name="Format">The image format for which to attempt retrieving the encoder</param>
/// <param name="Encoder">The encoder object in which to store the encoder if found</param>
/// <returns>True if the encoder was found, else false</returns>
private bool TryGetEncoder(GraphicFormats Format, out ImageCodecInfo Encoder)
{
ImageCodecInfo[] codecs;
codecs = ImageCodecInfo.GetImageEncoders();
Encoder = codecs.First(c => c.FormatDescription.Equals(Format.ToString(), StringComparison.CurrentCultureIgnoreCase));
return Encoder != null;
}
/// <summary>
/// Set the zoom level to view the entire image in the control
/// </summary>
public void ZoomExtents()
{
if (this.Image == null)
return;
this.ZoomFactor = (float)Math.Min((double)this.Width / this.Image.Width, (double)this.Height / this.Image.Height);
this.LimitBasePoint(imageLocation.X, imageLocation.Y);
this.Invalidate();
}
/// <summary>
/// Multiply the zoom
/// </summary>
/// <param name="NewZoomFactor">The zoom factor to set for the image</param>
public void SetZoom(float NewZoomFactor)
{
this.SetZoom(NewZoomFactor, Point.Empty);
}
/// <summary>
/// Multiply the zoom
/// </summary>
/// <param name="NewZoomFactor">The zoom factor to set for the image</param>
/// <param name="ZoomLocation">The point in which to zoom in</param>
public void SetZoom(float NewZoomFactor, Point ZoomLocation)
{
int x;
int y;
float multiplier;
multiplier = NewZoomFactor / this.ZoomFactor;
x = (int)((ZoomLocation.IsEmpty ? this.Width / 2 : ZoomLocation.X - imageLocation.X) / ZoomFactor);
y = (int)((ZoomLocation.IsEmpty ? this.Height / 2 : ZoomLocation.Y - imageLocation.Y) / ZoomFactor);
if ((multiplier < 1 && this.ZoomFactor > this.MinimumZoomFactor) || (multiplier > 1 && this.ZoomFactor < this.MaximumZoomFactor))
ZoomFactor *= multiplier;
else
return;
LimitBasePoint((int)(this.Width / 2 - x * ZoomFactor), (int)(this.Height / 2 - y * ZoomFactor));
this.Invalidate();
}
/// <summary>
/// Determines the base point for positioning the image
/// </summary>
/// <param name="x">The x coordinate based on which to determine the positioning</param>
/// <param name="y">The y coordinate based on which to determine the positioning</param>
private void LimitBasePoint(int x, int y)
{
int width;
int height;
if (this.Image == null)
return;
width = this.Width - (int)(Image.Width * ZoomFactor);
height = this.Height - (int)(Image.Height * ZoomFactor);
x = width < 0 ? Math.Max(Math.Min(x, 0), width) : width / 2;
y = height < 0 ? Math.Max(Math.Min(y, 0), height) : height / 2;
imageLocation = new Point(x, y);
}
/// <summary>
/// Verify that the maximum and minimum zoom are correctly set
/// </summary>
private void SetZoomFactorLimits()
{
float maximum = this.MaximumZoomFactor;
float minimum = this.minimumZoomFactor;
this.maximumZoomFactor = Math.Max(maximum, minimum);
this.minimumZoomFactor = Math.Min(maximum, minimum);
}
/// <summary>
/// Mouse button down event
/// </summary>
protected override void OnMouseDown(MouseEventArgs e)
{
switch (this.ZoomMode)
{
case ImageViewerZoomMode.OnClick:
switch (e.Button)
{
case MouseButtons.Left:
this.SetZoom(this.ZoomFactor * this.ZoomStep, e.Location);
break;
case MouseButtons.Middle:
this.panning = true;
this.Cursor = Cursors.NoMove2D;
this.imageTranslation = e.Location;
break;
case MouseButtons.Right:
this.SetZoom(this.ZoomFactor / this.ZoomStep, e.Location);
break;
}
break;
case ImageViewerZoomMode.Lens:
if (e.Button == MouseButtons.Left)
{
this.Cursor = Cursors.Cross;
this.Lens.Location = e.Location;
this.Lens.Visible = true;
}
else
{
this.Cursor = Cursors.Default;
this.Lens.Visible = false;
}
this.Invalidate();
break;
}
base.OnMouseDown(e);
}
/// <summary>
/// Mouse button up event
/// </summary>
protected override void OnMouseUp(MouseEventArgs e)
{
switch (this.ZoomMode)
{
case ImageViewerZoomMode.OnClick:
if (e.Button == MouseButtons.Middle)
{
panning = false;
this.Cursor = Cursors.Default;
}
break;
case ImageViewerZoomMode.Lens:
break;
}
base.OnMouseUp(e);
}
/// <summary>
/// Mouse move event
/// </summary>
protected override void OnMouseMove(MouseEventArgs e)
{
switch (this.ViewMode)
{
case ImageViewerViewMode.Normal:
switch (this.ZoomMode)
{
case ImageViewerZoomMode.OnClick:
if (panning)
{
LimitBasePoint(imageLocation.X + e.X - this.imageTranslation.X, imageLocation.Y + e.Y - this.imageTranslation.Y);
this.imageTranslation = e.Location;
}
break;
case ImageViewerZoomMode.Lens:
if (this.Lens.Visible)
{
this.Lens.Location = e.Location;
}
break;
}
break;
case ImageViewerViewMode.PrintSelection:
break;
case ImageViewerViewMode.PrintPreview:
break;
}
base.OnMouseMove(e);
}
/// <summary>
/// Resize event
/// </summary>
protected override void OnResize(EventArgs e)
{
LimitBasePoint(imageLocation.X, imageLocation.Y);
this.Invalidate();
base.OnResize(e);
}
/// <summary>
/// Paint event
/// </summary>
protected override void OnPaint(PaintEventArgs pe)
{
Rectangle src;
Rectangle dst;
pe.Graphics.Clear(this.BackColor);
if (this.Image != null)
{
switch (this.ViewMode)
{
case ImageViewerViewMode.Normal:
src = new Rectangle(Point.Empty, new Size(Image.Width, Image.Height));
dst = new Rectangle(this.imageLocation, new Size((int)(this.Image.Width * this.ZoomFactor), (int)(this.Image.Height * this.ZoomFactor)));
pe.Graphics.DrawImage(this.Image, dst, src, GraphicsUnit.Pixel);
this.Lens.Draw(pe.Graphics, this.Image, this.ZoomFactor, this.imageLocation);
break;
case ImageViewerViewMode.PrintSelection:
src = new Rectangle(Point.Empty, new Size(Image.Width, Image.Height));
dst = new Rectangle(this.imageLocation, new Size((int)(this.Image.Width * this.ZoomFactor), (int)(this.Image.Height * this.ZoomFactor)));
pe.Graphics.DrawImage(this.Image, dst, src, GraphicsUnit.Pixel);
break;
case ImageViewerViewMode.PrintPreview:
break;
}
}
//Debug.WriteLine("Viewer redrawn " + DateTime.Now);
base.OnPaint(pe);
}
}
EDIT 3:
Experience further graphics-related trouble when setting the height to something large. For example, if in the AreaSelection constructor I set the height to 500, dragging the control really screws up the painting.
whilst dragging the underlying image which shows through the rectangle gets lags behind
This is rather inevitable, updating the rectangle also redraws the image. And if that's expensive, say more than 30 milliseconds, then this can become noticeable to the eye.
That's a lot of milliseconds for something as simple as an image on a modern machine. The only way it can take that long is when the image is large and needs to be rescaled to fit the picturebox. And the pixel format is incompatible with the pixel format of the video adapter so that every single one of them has to be translated from the image pixel format to the video adapter's pixel format. That can indeed add up to multiple milliseconds.
You'll need to help to avoid PictureBox from having to burn that many cpu cycles every time the image gets painted. Do so by prescaling the image, turning it from a huge bitmap into one that better fits the control. And by altering the pixel format, the 32bppPArgb format is best by a long shot since that matches the pixel format of the vast majority of all video adapters. It draws ten times faster than all the other formats. You'll find boilerplate code to make this conversion in this answer.
What is a good algorithm for pixelating an image in C# .NET?
A simple, yet unefficient solution would be to resize to a smaller size, then resize back using pixel duplication.
A better solution would be (pseudo-code):
(Time O(n), Additional space (besides mutable source image): O(1))
// Pixelize in x axis (choose a whole k s.t. 1 <= k <= Width)
var sum = Pixel[0, 0];
for (y = 0; y < Height; y++)
{
for (x = 0; x < Width + 1; x++)
{
if (x % k == 0)
{
sum /= k;
for (xl = Max(0, x-k); xl < x; xl++)
Pixel[y, xl] = sum;
sum = 0;
}
if (x == Width)
break;
sum += Pixel[y, x];
}
}
// Now do the same in the y axis
// (make sure to keep y the outer loop - for better performance)
// If your image has more than one channel, then then Pixel should be a struct.
The guy over at this forum has a pretty good algorithm. It works by taking the average of all of the colors in each "block."
I just used his implementation in C#/GDI+ today:
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Linq;
using System.Text;
/// <summary>
/// Applies a pixelation effect to an image.
/// </summary>
[SuppressMessage(
"Microsoft.Naming",
"CA1704",
Justification = "'Pixelate' is a word in my book.")]
public class PixelateEffect : EffectBase
{
/// <summary>
/// Gets or sets the block size, in pixels.
/// </summary>
private int blockSize = 10;
/// <summary>
/// Gets or sets the block size, in pixels.
/// </summary>
public int BlockSize
{
get
{
return this.blockSize;
}
set
{
if (value <= 1)
{
throw new ArgumentOutOfRangeException("value");
}
this.blockSize = value;
}
}
/// <summary>
/// Applies the effect by rendering it onto the target bitmap.
/// </summary>
/// <param name="source">The source bitmap.</param>
/// <param name="target">The target bitmap.</param>
public override void DrawImage(Bitmap source, Bitmap target)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (target == null)
{
throw new ArgumentNullException("target");
}
if (source.Size != target.Size)
{
throw new ArgumentException("The source bitmap and the target bitmap must be the same size.");
}
using (var graphics = Graphics.FromImage(target))
{
graphics.PageUnit = GraphicsUnit.Pixel;
for (int x = 0; x < source.Width; x += this.BlockSize)
{
for (int y = 0; y < source.Height; y += this.BlockSize)
{
var sums = new Sums();
for (int xx = 0; xx < this.BlockSize; ++xx)
{
for (int yy = 0; yy < this.BlockSize; ++yy)
{
if (x + xx >= source.Width || y + yy >= source.Height)
{
continue;
}
var color = source.GetPixel(x + xx, y + yy);
sums.A += color.A;
sums.R += color.R;
sums.G += color.G;
sums.B += color.B;
sums.T++;
}
}
var average = Color.FromArgb(
sums.A / sums.T,
sums.R / sums.T,
sums.G / sums.T,
sums.B / sums.T);
using (var brush = new SolidBrush(average))
{
graphics.FillRectangle(brush, x, y, (x + this.BlockSize), (y + this.BlockSize));
}
}
}
}
}
/// <summary>
/// A structure that holds sums for color averaging.
/// </summary>
private struct Sums
{
/// <summary>
/// Gets or sets the alpha component.
/// </summary>
public int A
{
get;
set;
}
/// <summary>
/// Gets or sets the red component.
/// </summary>
public int R
{
get;
set;
}
/// <summary>
/// Gets or sets the blue component.
/// </summary>
public int B
{
get;
set;
}
/// <summary>
/// Gets or sets the green component.
/// </summary>
public int G
{
get;
set;
}
/// <summary>
/// Gets or sets the total count.
/// </summary>
public int T
{
get;
set;
}
}
}
Caveat emptor, works on my machine, & etc.
While I don't know of a well know algorithm for this, I did have to write something similar. The technique I used was pretty simple, but I am thinking not very efficient for large images. Basically I would take the image and do color averaging in 5 (or howerver big you want) pixel blocks and then make all those pixels the same color. You could speed this up by doing the average on just the diagonal pixels which would save a lot of cycles but be less accurate.