Как запретить UserControl (в девичестве ScrollableControl) вызывать ScrollWindow?

.NET UserControl (который происходит от _ 2_) должен отображать горизонтальное и вертикальное полосы прокрутки.

Вызывающий может установить видимость и диапазон этих горизонтальных и вертикальных полос прокрутки:

UserControl.AutoScroll = true;
UserControl.AutoScrollMinSize = new Size(1000, 4000); //1000x4000 scroll area

Примечание. UserControl (т.е. ScrollableControl) использует стандартный механизм Windows для указания стилей окна WS_HSCROLL и WS_VSCROLL для отображения полос прокрутки. То есть: они не создают отдельные элементы управления прокруткой Windows или .NET, размещая их в правой / нижней части окна. В Windows есть стандартный механизм для отображения одной или обеих полос прокрутки.

Если пользователь прокручивает элемент управления, UserControl отправляется сообщение WM_HSCROLL или WM_VSCROLL. В ответ на эти сообщения я хочу, чтобы ScrollableControl аннулировал клиентскую область, что произошло бы в собственном Win32:

switch (uMsg) 
{ 
   case WM_VSCROLL:
       ...
       GetScrollInfo(...);
       ...
       SetScrollInfo(...);
       ...

       InvalidateRect(g_hWnd, 
              null, //erase entire client area
              true, //background needs erasing too (trigger WM_ERASEBKGND));
       break;
 }

мне нужно, чтобы вся клиентская область была недействительной. Проблема в том, что UserControl (т.е. ScrollableControl) вызывает ScrollWindow Функция API:

protected void SetDisplayRectLocation(int x, int y)
{
    ...
    if ((nXAmount != 0) || ((nYAmount != 0) && base.IsHandleCreated))
    {
        ...
        SafeNativeMethods.ScrollWindowEx(new HandleRef(this, base.Handle), nXAmount, nYAmount, null, ref rectClip, NativeMethods.NullHandleRef, ref prcUpdate, 7);
    }
    ...
}

Вместо того, чтобы запускать InvalidateRect для всего клиентского прямоугольника, ScrollableControl пытается «спасти» существующее содержимое в клиентской области. Например, пользователь прокручивает вверх, текущее содержимое клиента сдвигается вниз на ScrollWindowEx, а затем становится недействительной только недавно обнаруженная область, вызывая WM_PAINT:

введите описание изображения здесь

На приведенной выше диаграмме область шахматной доски - это контент, который недействителен и должен быть закрашен во время следующего WM_PAINT.

В моем случае это нехорошо; верхняя часть моего элемента управления содержит «заголовок» (например, заголовки столбцов списка). Прокрутка этого содержимого дальше вниз неверна:

введите описание изображения здесь

и это вызывает визуальное искажение.

я хочу, чтобы ScrollableControl не использовал ScrollWindowEx, а вместо этого просто аннулировал всю клиентскую область.

Я пробовал переопределить OnScroll защищенный метод:

protected override void OnScroll(ScrollEventArgs se)
{
   base.OnScroll(se);

   this.Invalidate();
}

Но это вызывает двойную ничью.

Примечание. я мог бы использовать двойную буферизацию, чтобы замаскировать проблему, но это не настоящее решение.

  • двойная буферизация не должна использоваться в сеансе удаленного рабочего стола / терминала
  • это расточительное использование ресурсов ЦП
  • это не тот вопрос, который я задаю

Я подумал об использовании Control вместо UserControl (т.е. до ScrollableControl в цепочке наследования) и вручную добавить элемент управления HScroll или VScroll .NET, но это тоже нежелательно:

  • Windows уже обеспечивает стандартный вид положения полос прокрутки (дублировать нетривиально)
  • это много функций, которые нужно воспроизводить с нуля, когда мне нужно только InvalidateRect, а не ScrollWindowEx

Поскольку я могу видеть и размещать внутренний код для ScrollableControl, я знаю, что нет свойства для отключения использования ScrollWindow, но есть ли свойство для отключения использования ScrollWindow?


Обновлять:

Я попытался переопределить метод нарушения и использовать отражатель для кражи всего кода:

protected override void SetDisplayRectLocation(int x, int y)
{
    ...
    Rectangle displayRect = this.displayRect;
    ...
    this.displayRect.X = x;
    this.displayRect.Y = y;
    if ((nXAmount != 0) || ((nYAmount != 0) && base.IsHandleCreated))
    {
        ...
        SafeNativeMethods.ScrollWindowEx(new HandleRef(this, base.Handle), nXAmount, nYAmount, null, ref rectClip, NativeMethods.NullHandleRef, ref prcUpdate, 7);
    }
    ...
}

Проблема в том, что SetDisplayRectLocation читает и записывает в частную переменную-член (displayRect). Если Microsoft не изменит C #, чтобы разрешить потомкам доступ к закрытым членам: я не могу этого сделать.


Обновление два

я понял, что копирование и вставка реализации ScrollableControl, исправление одной проблемы означает, что мне также придется копировать-n-вставить всю цепочку наследования до UserControl

...
   ScrollableControl2 : Control, IArrangedElement, IComponent, IDisposable
      ContainerControl2 : ScrollableControl2, IContainerControl
         UserControl2 : ContainerControl2

я бы предпочел работать с объектно-ориентированным дизайном, а не против него.


person Ian Boyd    schedule 25.04.2011    source источник
comment
Не могли бы вы предоставить скриншоты без нецензурной лексики?   -  person David Heffernan    schedule 25.04.2011
comment
Вот так; девственные глаза спасены.   -  person Ian Boyd    schedule 25.04.2011
comment
Хороший вопрос. Вы, наверное, пробовали многое из того, что сделал бы я. Я полагаю, вы пытались переопределить WndPrc и перехватить как сообщение прокрутки, так и сообщения рисования?   -  person Pedery    schedule 25.04.2011
comment
@Pedery, я думал об этом, но тогда я все равно столкнулся бы с необходимостью повторной реализации большого объема обработки. Лучше всего просто переопределить оскорбительный защищенный метод SetDisplayLocation, не вызывая base, и скопировать все кишки, кроме одной строки. Обратной стороной этого решения является а) я бы предпочел работать с .NET UserControl (если возможно), а не против него, б) копируя текущую реализацию, я потеряю любые будущие улучшения .NET для ScrollableControl и c) хотя мне все равно: это незаконно   -  person Ian Boyd    schedule 25.04.2011
comment
Ага, понятно. Мне пришлось бы самому поиграть с этим, чтобы попытаться найти решение. Вы, кажется, полностью соответствуете возможностям и ограничениям .Net, поэтому я думаю, что ваши попытки так же хороши, как и мои. Удачи!   -  person Pedery    schedule 25.04.2011
comment
Типично объектно-ориентированный подход: защищенный SetDisplayLocation метод использует закрытый displayRect член - нет возможности повторно реализовать метод без доступа к этому закрытому члену.   -  person Ian Boyd    schedule 26.04.2011
comment
@ Ян, если я правильно понимаю, вы делаете всю работу по внутренней раскраске самостоятельно (поскольку вы не размещаете другие элементы управления внутри прокручиваемой области), поэтому изобретать колесо здесь действительно не так уж много, и это даст вы полностью контролируете области, которые хотите визуализировать. Если у вас есть внутренние элементы управления (кроме заголовков), это немного сложнее, но вы можете прокручивать только один большой контейнерный элемент управления вместо того, чтобы манипулировать позициями каждого элемента управления. Я бы выбрал (и выбрал раньше ...) один из этих вариантов.   -  person Itai Bar-Haim    schedule 19.02.2012
comment
Очень хорошо написанный, подробный пост. У меня точно такая же проблема, но я очень сомневаюсь, что я мог бы ее сформулировать. Было бы неплохо, если бы Microsoft предоставила возможность управлять этим аспектом ScrollableControl.   -  person Jim Balkwill    schedule 14.04.2015


Ответы (3)


У меня была такая же проблема, спасибо, что разместили это. Возможно, я нашел решение твоей проблемы. Мое решение - перегрузить WndProc для обработки сообщений прокрутки, отключить перерисовку при вызове обработчика базового класса, а затем принудительно перерисовать все окно после обработки сообщения. Это решение работает нормально:

    private void sendRedrawMessage( bool redrawFlag )
    {
        const int WM_SETREDRAW = 0x000B;

        IntPtr wparam = new IntPtr( redrawFlag ? 1 : 0 );
        Message msg = Message.Create( Handle, WM_SETREDRAW, wparam, IntPtr.Zero );
        NativeWindow.FromHandle( Handle ).DefWndProc( ref msg );
    }

    protected override void WndProc( ref Message m )
    {
        switch ( m.Msg )
        {
            case 276: // WM_HSCROLL
            case 277: // WM_VSCROLL
                sendRedrawMessage( false );
                base.WndProc( ref m );
                sendRedrawMessage( true );
                Refresh(); // Invalidate all
                return;
        }

        base.WndProc( ref m );
    }

Я подумал о том, чтобы попробовать это из-за предложения перегрузить WndProc в сочетании с вашим наблюдением, что вы не можете перегрузить SetDisplayRectLocation. Я думал, что отключение WM_PAINT во время обработки события прокрутки UserControl может сработать.

Надеюсь это поможет.

Том

person Tom    schedule 06.07.2013
comment
Спасибо, отлично работает! Возможно, вы захотите обработать WM_MOUSEWHEEL таким же образом; просто добавьте case 522: // WM_MOUSEWHEEL к переключателю в WndProc () - person Ricardo Massaro; 21.11.2014
comment
@ Рикардо. Хороший. Я использую колесико мыши для управления масштабированием изображения, и это именно тот сценарий, в котором мне нужно избегать ScrollWindowEx. - person Jim Balkwill; 14.04.2015

Вы пробовали связаться с программистом из Microsoft? Я уверен, что если вы обратитесь в Microsoft, вы сможете задать им свой вопрос или, возможно, даже получить поддержку по телефону.

Вот ссылка на поддержку платформы .NET: щелкните здесь. В нем упоминается, что вы можете связаться со специалистами службы поддержки .NET по электронной почте, телефону или через Интернет.

person Veridian    schedule 03.03.2012

Решение Тома отличное, но я думаю, что есть возможность для небольшой оптимизации. Без двух методов Тома, когда я вызываю прокрутку, например, щелкая конечную точку полосы прокрутки, мой onPaint видит единственный вызов. Когда я добавляю два метода Тома, мой onPaint начинает получать два вызова для одних и тех же позиций полосы прокрутки. Решение для меня, казалось, состояло в том, чтобы игнорировать последний SB_ENDSCROLL, который происходит, это операции прокрутки. Благодаря этому я перестал видеть повторяющиеся краски в одном и том же месте прокрутки.

private void sendRedrawMessage(bool redrawFlag)
{
    const int WM_SETREDRAW = 0x000B;

    IntPtr wparam = new IntPtr(redrawFlag ? 1 : 0);
    Message msg = Message.Create(Handle, WM_SETREDRAW, wparam, IntPtr.Zero);
    NativeWindow.FromHandle(Handle).DefWndProc(ref msg);
}

protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case 276: // WM_HSCROLL
        case 277: // WM_VSCROLL
            if ((ushort)m.WParam == 8) // SB_ENDSCROLL ignore scroll bar release
                break;
            sendRedrawMessage(false);
            base.WndProc(ref m);
            sendRedrawMessage(true);
            Refresh(); // Invalidate all
            return;
    }

    base.WndProc(ref m);
}
person Cameron    schedule 01.08.2015
comment
Я думаю, что если вы сделаете эту оптимизацию, окно не будет правильно перерисовываться после использования кнопок на концах полос прокрутки. По крайней мере, так мне показалось. - person BlueMonkMN; 07.01.2017