Como corrigir a oscilação nos controles do usuário


107

Em meu aplicativo, estou mudando constantemente de um controle para outro. Eu criei não. dos controles do usuário, mas durante a navegação meus controles piscam. leva 1 ou 2 segundos para atualizar. Eu tentei definir isso

SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
or
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); 
SetStyle(ControlStyles.DoubleBuffer, true);

mas não ajudou ... Cada controle tem a mesma imagem de fundo com controles diferentes. Então, qual é a solução para isso ..
Obrigado.


Onde estão essas declarações? O ideal é colocá-los no construtor. Você ligou UpdateStylesdepois de definir isso? Está mal documentado, mas às vezes pode ser necessário.
Thomas,

Respostas:


306

Não é o tipo de cintilação que o buffer duplo pode resolver. Nem BeginUpdate ou SuspendLayout. Você tem muitos controles, o BackgroundImage pode torná-lo muito pior.

Ele começa quando o UserControl se pinta. Ele desenha o BackgroundImage, deixando buracos onde vão as janelas de controle filho. Cada controle filho recebe uma mensagem para pintar a si mesmo, eles preencherão a lacuna com o conteúdo da janela. Quando você tem muitos controles, esses buracos ficam visíveis para o usuário por um tempo. Eles são normalmente brancos, contrastando mal com BackgroundImage quando está escuro. Ou podem ser pretos se o formulário tiver a propriedade Opacity ou TransparencyKey definida, contrastando mal com quase tudo.

Esta é uma limitação fundamental do Windows Forms, ela está presa na maneira como o Windows renderiza as janelas. Corrigido por WPF btw, ele não usa janelas para controles filho. O que você deseja é o buffer duplo de todo o formulário, incluindo os controles filhos. Isso é possível, verifique meu código neste segmento para a solução. Porém, tem efeitos colaterais e não aumenta a velocidade de pintura. O código é simples, cole no seu formulário (não no controle do usuário):

protected override CreateParams CreateParams {
  get {
    CreateParams cp = base.CreateParams;
    cp.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    return cp;
  }
} 

Há muitas coisas que você pode fazer para melhorar a velocidade de pintura, a ponto de a cintilação não ser mais perceptível. Comece abordando o BackgroundImage. Eles podem ser muito caros quando a imagem de origem é grande e precisa ser reduzida para caber no controle. Altere a propriedade BackgroundImageLayout para "Tile". Se isso fornecer uma aceleração perceptível, volte para o seu programa de pintura e redimensione a imagem para uma melhor correspondência com o tamanho de controle típico. Ou escreva o código no método OnResize () do UC para criar uma cópia de tamanho adequado da imagem para que ela não precise ser redimensionada toda vez que o controle redesenhar. Use o formato de pixel Format32bppPArgb para essa cópia, ele renderiza cerca de 10 vezes mais rápido do que qualquer outro formato de pixel.

A próxima coisa que você pode fazer é evitar que os buracos sejam tão perceptíveis e contrastem mal com a imagem. Você pode desativar o sinalizador de estilo WS_CLIPCHILDREN para o UC, o sinalizador que impede o UC de pintar na área onde os controles filhos vão. Cole este código no código do UserControl:

protected override CreateParams CreateParams {
  get {
    var parms = base.CreateParams;
    parms.Style &= ~0x02000000;  // Turn off WS_CLIPCHILDREN
    return parms;
  }
}

Os controles filhos agora vão se pintar no topo da imagem de fundo. Você ainda pode vê-los se pintando um por um, mas o feio buraco branco intermediário ou negro não será visível.

Por último, mas não menos importante, reduzir o número de controles filho é sempre uma boa abordagem para resolver problemas de pintura lenta. Substitua o evento OnPaint () do UC e desenhe o que agora é mostrado em um filho. Rótulo específico e PictureBox são muito desperdiçadores. Conveniente para apontar e clicar, mas sua alternativa leve (desenhar uma string ou uma imagem) leva apenas uma única linha de código em seu método OnPaint ().


Desative WS_CLIPCHILDREN experiência do usuário aprimorada para mim.
Mahesh

Absolutamente perfeito! .. Muito obrigado
AlejandroAlis

8

Este é um problema real, e a resposta que Hans Passant deu é ótima para salvar a cintilação. No entanto, há efeitos colaterais, como ele mencionou, e eles podem ser feios (IU feia). Conforme declarado, "Você pode desligar oWS_CLIPCHILDREN sinalizador de estilo para a UC", mas isso só o desativa para uma UC. Os componentes do formulário principal ainda apresentam problemas.

Por exemplo, uma barra de rolagem do painel não pinta, porque está tecnicamente na área filho. No entanto, o componente filho não desenha a barra de rolagem, então ela não é pintada até que o mouse passe (ou outro evento o acione).

Além disso, os ícones animados (alterando ícones em um loop de espera) não funcionam. Remover ícones em um tabPage.ImageKeynão redimensiona / redesenha as outras páginas da guia apropriadamente.

Então, eu estava procurando uma maneira de desligar a WS_CLIPCHILDRENpintura inicial para que meu Form carregue bem pintado, ou melhor ainda, apenas ligue-o enquanto redimensiona meu formulário com muitos componentes.

O truque é fazer com que o aplicativo chame CreateParamscom o WS_EX_COMPOSITED/WS_CLIPCHILDRENestilo desejado . Encontrei um hack aqui ( https://web.archive.org/web/20161026205944/http://www.angryhacker.com/blog/archive/2010/07/21/how-to-get-rid-of- flicker-on-windows-forms-applications.aspx ) e funciona muito bem. Obrigado AngryHacker!

Eu coloco a TurnOnFormLevelDoubleBuffering()chamada no ResizeBeginevento de formulário e TurnOffFormLevelDoubleBuffering()chamo o evento ResizeEnd do formulário (ou apenas o deixo WS_CLIPCHILDRENdepois que ele é inicialmente pintado corretamente).

    int originalExStyle = -1;
    bool enableFormLevelDoubleBuffering = true;

    protected override CreateParams CreateParams
    {
        get
        {
            if (originalExStyle == -1)
                originalExStyle = base.CreateParams.ExStyle;

            CreateParams cp = base.CreateParams;
            if (enableFormLevelDoubleBuffering)
                cp.ExStyle |= 0x02000000;   // WS_EX_COMPOSITED
            else
                cp.ExStyle = originalExStyle;

            return cp;
        }
    }

    public void TurnOffFormLevelDoubleBuffering()
    {
        enableFormLevelDoubleBuffering = false;
        this.MaximizeBox = true;
    }

Seu código não inclui o método TurnOnFormLevelDoubleBuffering () ...
Dan W

@DanW Dê uma olhada no URL postado nesta resposta ( angryhacker.com/blog/archive/2010/07/21/… )
ChrisB

O link nesta resposta parece estar morto. Estou curioso para saber a solução, você tem um link para outro exemplo?
Pratt Hinds

6

Se você estiver fazendo qualquer pintura personalizada no controle (ou seja, substituindo OnPaint), você mesmo pode tentar o buffer duplo.

Image image;
protected override OnPaint(...) {
    if (image == null || needRepaint) {
        image = new Bitmap(Width, Height);
        using (Graphics g = Graphics.FromImage(image)) {
            // do any painting in image instead of control
        }
        needRepaint = false;
    }
    e.Graphics.DrawImage(image, 0, 0);
}

E invalidar seu controle com uma propriedade NeedRepaint

Caso contrário, a resposta acima com SuspendLayout e ResumeLayout é provavelmente o que você deseja.


Este é um método criativo para simular doublebuffer !. Você pode adicionar if (image != null) image.Dispose();antesimage = new Bitmap...
S.Serpooshan


2

No formulário principal ou controle do usuário onde a imagem de fundo reside, defina a BackgroundImageLayoutpropriedade como Centerou Stretch. Você notará uma grande diferença quando o controle do usuário estiver renderizando.


2

Tentei adicionar isso como um comentário, mas não tenho pontos suficientes. Esta é a única coisa que ajudou meus problemas intermitentes, muito obrigado a Hans por sua postagem. Para qualquer um que esteja usando o c ++ builder como eu, aqui está a tradução

Adicione a declaração CreateParams ao arquivo .h do formulário principal do seu aplicativo, por exemplo

class TYourMainFrom : public TForm
{
protected:
    virtual void __fastcall CreateParams(TCreateParams &Params);
}

e adicione isso ao seu arquivo .cpp

void __fastcall TYourMainForm::CreateParams(TCreateParams &Params)
{
    Params.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    TForm::CreateParams(Params);
}

2

Coloque o código abaixo em seu construtor ou evento OnLoad e se você estiver usando algum tipo de controle de usuário personalizado com subcontroles, você precisará certificar-se de que esses controles personalizados também tenham buffer duplo (embora na documentação da MS eles digam é definido como verdadeiro por padrão).

Se você estiver fazendo um controle personalizado, convém adicionar este sinalizador em seu ctor:

SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

Opcionalmente, você pode usar este código em seu formulário / controle:

foreach (Control control in Controls)
{
    typeof(Control).InvokeMember("DoubleBuffered",
        BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
        null, control, new object[] { true });
}

Nós iteramos por todos os controles no formulário / controle e acessamos suas DoubleBufferedpropriedades e então mudamos para verdadeiro a fim de tornar cada controle no formulário em buffer duplo. A razão de fazermos reflexão aqui é porque imagine que você tem um controle que possui controles filhos que não são acessíveis, dessa forma, mesmo que sejam controles privados, ainda alteraremos sua propriedade para true.

Mais informações sobre a técnica de buffer duplo podem ser encontradas aqui .

Há outra propriedade que geralmente substituo para resolver esse problema:

protected override CreateParams CreateParams
{
    get
    {
        CreateParams parms = base.CreateParams;
        parms.ExStyle |= 0x00000020; // WS_EX_COMPOSITED
        return parms;
    }
}

WS_EX_COMPOSITED - Pinta todos os descendentes de uma janela na ordem de pintura de baixo para cima usando buffer duplo.

Você pode encontrar mais desses sinalizadores de estilo aqui .

Espero que ajude!


1

Só para acrescentar à resposta que Hans deu:

(Versão TLDR: a transparência é mais pesada do que você pensa, use apenas cores sólidas em todos os lugares)

Se WS_EX_COMPOSITED, DoubleBuffered e WS_CLIPCHILDREN não resolveram seu flicker (para mim WS_CLIPCHILDREN tornou ainda pior), tente isto: passe por TODOS os seus controles e todo o seu código, e onde quer que você tenha Qualquer transparência ou semitransparência para BackColor, ForeColor ou qualquer outra cor, basta removê-la, usar apenas cores sólidas. Na maioria dos casos em que você acha que só precisa usar transparência, não o faz. Reprojete seu código e controles e use cores sólidas. Tive uma tremulação terrível e terrível e o programa estava lento. Depois de remover a transparência, ela aumentou significativamente e não há oscilação.

EDIT: Para adicionar mais, acabei de descobrir que WS_EX_COMPOSITED não precisa ser de toda a janela, ele pode ser aplicado apenas a controles específicos! Isso me salvou de muitos problemas. Basta fazer um controle customizado herdado de qualquer controle de que você precisa e colar a substituição já postada para WS_EX_COMPOSITED. Desta forma, você obtém buffer duplo de baixo nível apenas neste controle, evitando os efeitos colaterais desagradáveis ​​no resto do aplicativo!


0

Sei que essa pergunta é muito antiga, mas quero passar minha experiência sobre ela.

Eu tive muitos problemas com Tabcontrolcintilação em um formulário com substituído OnPainte / ou OnPaintBackGroundno Windows 8 usando .NET 4.0.

A única coisa que funcionou foi NÃO USAR o Graphics.DrawImagemétodo em OnPaintoverrides, ou seja, quando o desenho foi feito diretamente nos Gráficos fornecidos pela PaintEventArgs, mesmo pintando todo o retângulo, a cintilação desapareceu. Mas se chamar o DrawImagemétodo, mesmo desenhando um bitmap recortado (criado para buffer duplo), a cintilação aparece.

Espero que ajude!


0

Combinei esta correção de oscilação e esta correção de fonte , então tive que adicionar um pouco do meu próprio código para iniciar um cronômetro na pintura para invalidar o TabControl quando ele sai da tela e volta, etc.

Todos os três fazem isso:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class TabControlEx:TabControl
{
    [DllImport("user32.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    private const int WM_PAINT = 0x0f;
    private const int WM_SETFONT = 0x30;
    private const int WM_FONTCHANGE = 0x1d;
    private System.Drawing.Bitmap buffer;
    private Timer timer = new Timer();
    public TabControlEx()
    {
        timer.Interval = 1;
        timer.Tick += timer_Tick;
        this.SetStyle(ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
    }
    void timer_Tick(object sender, EventArgs e)
    {
        this.Invalidate();
        this.Update();
        timer.Stop();
    }
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_PAINT) timer.Start();
        base.WndProc(ref m);
    }
    protected override void OnPaint(PaintEventArgs pevent)
    {
        this.SetStyle(ControlStyles.UserPaint, false);
        base.OnPaint(pevent);
        System.Drawing.Rectangle o = pevent.ClipRectangle;
        System.Drawing.Graphics.FromImage(buffer).Clear(System.Drawing.SystemColors.Control);
        if (o.Width > 0 && o.Height > 0)
        DrawToBitmap(buffer, new System.Drawing.Rectangle(0, 0, Width, o.Height));
        pevent.Graphics.DrawImageUnscaled(buffer, 0, 0);
        this.SetStyle(ControlStyles.UserPaint, true);
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        buffer = new System.Drawing.Bitmap(Width, Height);
    }
    protected override void OnCreateControl()
    {
        base.OnCreateControl();
        this.OnFontChanged(EventArgs.Empty);
    }
    protected override void OnFontChanged(EventArgs e)
    {
        base.OnFontChanged(e);
        IntPtr hFont = this.Font.ToHfont();
        SendMessage(this.Handle, WM_SETFONT, hFont, (IntPtr)(-1));
        SendMessage(this.Handle, WM_FONTCHANGE, IntPtr.Zero, IntPtr.Zero);
        this.UpdateStyles();
    }
}

Eu não sou o criador, mas pelo que entendi, o bitmap faz todo o desvio de bug.

Esta foi a única coisa que resolveu definitivamente a cintilação do TabControl (com ícones) para mim.

vídeo de resultado de diferença: vanilla tabcontrol vs tabcontrolex

http://gfycat.com/FineGlitteringDeermouse

ps. você precisará definir HotTrack = true, porque isso também corrige o bug


-2

Você tentou a Control.DoubleBufferedpropriedade?

Obtém ou define um valor que indica se este controle deve redesenhar sua superfície usando um buffer secundário para reduzir ou evitar cintilação.

Também isso e isso podem ajudar.


-9

Não há necessidade de nenhum buffer duplo e todas essas coisas caras ...

Uma solução simples ...

Se você estiver usando a interface MDI, basta colar o código abaixo no formulário principal. Isso removerá todas as cintilações das páginas. No entanto, algumas páginas que requerem mais tempo para carregar aparecerão em 1 ou 2 segundos. Mas isso é melhor do que mostrar uma página piscando em que cada item vem um por um.

Esta é a única melhor solução para toda a aplicação. Veja o código para colocar no formulário principal:

protected override CreateParams CreateParams {
  get {
    CreateParams cp = base.CreateParams;
    cp.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    return cp;
  }
} 

12
Então, o que você está dizendo é que a resposta que Hans deu há mais de dois anos está, de fato, correta? Obrigado, Kshitiz. Isso é realmente muito útil!
Fernando,
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.