views:

1217

answers:

3

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.

+1  A: 

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;
        }
    }
}
Treb
A: 

@Treb

Your solution is working fine. And thanks to post the entire code here. I have got link to Nicke's blog everywhere but that link was not opening. But it has problems when selected range is more than one month. I am working on solving that and will post the code here once it is done.

devcoder
A: 

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 - 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.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.

Mark Cranness