Este artigo foi traduzido por máquina.

Fronteiras da interface do usuário

Toque e resposta

Charles Petzold

Baixe o código de exemplo

Charles PetzoldA programação é uma disciplina engenharia em vez de uma ciência ou uma ramificação de matemática, portanto, raramente existem uma única solução correta para um problema. Variedades e as variações são a norma e freqüentemente é pois resolvem para explorar essas alternativas em vez de enfocar um método particular.

Em meu artigo “ Multi-Touch eventos de manipulação no WPF ” na edição de agosto da MSDN Magazine, comecei a explorar o suporte multi-touch empolgante introduzido na versão 4 do Windows Presentation Foundation (WPF). Os eventos de manipulação de servem principalmente para consolidar multi-touch inseridos no útil transformações geométricas e para ajudar na implementação de inércia.

Nesse artigo, mostrei a que dois relacionados métodos para manipulação de manipulação de eventos em uma coleção de elementos de imagem. Em ambos os casos, os eventos reais foram processados pela classe de janela. Um programa definido manipuladores para os eventos de manipulação dos elementos manipulated. A outra abordagem mostrou como substituir os métodos OnManipulation para obter os mesmos eventos roteados por meio da árvore visual.

A abordagem de classe personalizada

Além disso, uma terceira abordagem faz sentido: Uma classe personalizada pode ser definida para os elementos de manipulated substitui seus próprios métodos OnManipulation em vez de deixar este trabalho para um elemento de contêiner. A vantagem dessa abordagem é que você pode criar a classe personalizada um pouco mais atrativo decorando-lo com uma borda ou outro elemento;dessas decorações também podem ser usadas para fornecer comentários visuais quando o usuário toca um elemento manipulável.

Quando os programadores WPF veterano determinam que precisam fazer alterações visuais em um controle com base em eventos, eles provavelmente pensa EventTrigger, mas os programadores WPF precisam para começar a fazer a transição para o Gerenciador de estado visual. Mesmo quando derivar de UserControl (a estratégia que estará usando), é relativamente fácil de implementar.

Um aplicativo usando os eventos de manipulação de provavelmente deve basear o comentário visual desses eventos mesmos em vez dos eventos de TouchDown e retoque de nível inferior. Ao usar os eventos de manipulação, você desejará começar o comentário visual com o ManipulationStarting ou ManipulationStarted evento. (Na verdade não faz diferença que você escolhe para este trabalho.)

No entanto, quando experimentar esse feedback, é uma das primeiras coisas que você descobrirá que os eventos ManipulationStarting e ManipulationStarted não são acionados quando um elemento, primeiro é tocado, mas somente quando ele começa a mover. Esse comportamento é um remanescente da interface de caneta e você vai querer alterá-la, definindo a propriedade anexada a seguir no elemento manipulated:

Stylus.IsPressAndHoldEnabled="False"

Agora, os eventos ManipulationStarting e ManipulationStarted são acionados quando um elemento é tocado pela primeira vez. Você desejará desativar o comentário visual com um a ManipulationInertiaStarting ou manipulação de ­ evento concluído, dependendo se você deseja que os comentários para finalizar a dedo do usuário levanta da tela ou após o elemento parou de movimentação devido à inércia. Se você não estiver usando inércia (como eu não será neste artigo), não importa qual evento você usa.

O código para download deste artigo está em uma única solução do Visual Studio chamada TouchAndResponseDemos com dois projetos. O primeiro projeto é denominado FeedbackAndSmoothZ, que inclui um derivativo do UserControl personalizado chamado ManipulablePictureFrame que implementa a lógica de manipulação.

ManipulablePictureFrame define uma única propriedade do tipo filho e usa seu construtor estático para redefinir os padrões para as três propriedades: HorizontalAlignment, VerticalAlignment e o IsManipulationEnabled all-important. O construtor de instância chama InitializeComponent (normalmente), mas, em seguida, define RenderTransform do controle como um MatrixTransform, se não for o caso.

Durante o evento de OnManipulationStarting manipulável ­ PictureFrame classe chama:

VisualStateManager.GoToElementState(this, "Touched", false);

e durante o evento OnManipulationCompleted, ele chama:

VisualStateManager.GoToElementState(this, "Untouched", false);

Este é de contribuição exclusiva do meu arquivo código para a implementação de estados visuais. O código executar manipulações de reais será familiar do código na coluna do mês passado, com duas alterações significativas:

  • Método OnManipulationStarting, o ManipulationContainer é definida como pai do elemento.
  • O método OnManipulationDelta é apenas um pouco mais simples, porque o elemento que está sendo manipulado é o próprio objeto de PictureFrame manipulável ­.

A Figura 1 mostra o arquivo ManipulablePictureFrame.xaml completo.

Figura 1 do Arquivo ManipulablePictureFrame.xaml  

<UserControl x:Class="FeedbackAndSmoothZ.ManipulablePictureFrame"
             xmlns=
       "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Stylus.IsPressAndHoldEnabled="False"
             Name="this">
    
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="TouchStates">
            <VisualState x:Name="Touched">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="maskBorder"
                                     Storyboard.TargetProperty="Opacity"
                                     To="0.33" Duration="0:0:0.25" />
                    
                    <DoubleAnimation Storyboard.TargetName="dropShadow"
                                     Storyboard.TargetProperty=          
                  "ShadowDepth"
                                     To="20" Duration="0:0:0.25" />
                </Storyboard>
            </VisualState>
            <VisualState x:Name="Untouched">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="maskBorder"
                                     Storyboard.TargetProperty="Opacity"
                                     To="0" Duration="0:0:0.1" />

                    <DoubleAnimation Storyboard.TargetName="dropShadow"
                                     Storyboard.TargetProperty=     
                      "ShadowDepth"
                                     To="5" Duration="0:0:0.1" />
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    
    <Grid>
        <Grid.Effect>
            <DropShadowEffect x:Name="dropShadow" />
        </Grid.Effect>
        
        <!-- Holds the photo (or other element) -->
        <Border x:Name="border" 
                Margin="24" />
        
        <!-- Provides visual feedback -->
        <Border x:Name="maskBorder" 
                Margin="24" 
                Background="White" 
                Opacity="0" />
        
        <!-- Draws the frame -->
        <Rectangle Stroke="{Binding ElementName=this, Path=Foreground}" 
                   StrokeThickness="24" 
                   StrokeDashArray="0 0.9" 
                   StrokeDashCap="Round" 
                   RadiusX="24" 
                   RadiusY="24" />
        
        <Rectangle Stroke="{Binding ElementName=this, Path=Foreground}" 
                   StrokeThickness="8" 
                   Margin="16" 
                   RadiusX="24" 
                   RadiusY="24" />
    </Grid>
</UserControl>

Borda chamada “ borda ” é usada para hospedar o filho da classe ManipulablePictureFrame. Isso provavelmente será um elemento Image, mas não tem que ser. Os dois elementos Rectangle desenhar um tipo de quadro “-recorte ” ao redor da borda e o segundo Border é usado para o comentário visual.

Embora um elemento que está sendo movido, as animações em ManipulablePictureFrame.xaml “ clarear ” a imagem um pouco — na verdade, é mais de um efeito “ eliminando ” — e aumentar a sombra, conforme mostrado no do Figura 2.

Figure 2 A Highlighted Element in the FeedbackAndSmoothZ Program

De de um elemento realçado no programa FeedbackAndSmoothZ, a Figura 2

Praticamente qualquer tipo de realce simples pode fornecer feedback visual durante eventos de contato. No entanto, se você estiver trabalhando com elementos pequenos que podem ser alterados e manipulados, convém aumentar os elementos quando eles são utilizados para não ficar ocultos inteiramente pelo finger do usuário. (Ativado por outro lado, você não que aumentar um elemento para o comentário visual se também estiver permitindo que o usuário redimensionar o elemento. É muito disconcerting para manipular um elemento em um tamanho desejado e, em seguida, fazer com que ele reduza um pouco ao que você retire os dedos na tela de!)

Você perceberá que à medida que você faz as imagens maiores e menores, o quadro reduz ou expande de acordo. Este é o comportamento correto? Talvez. Talvez não. Mostrarei uma alternativa para esse comportamento no final deste artigo.

Suavizar as transições de Z

Nos programas que mostrei no mês passado, tocar em uma foto faria com que ele vá para o primeiro plano. Essa foi praticamente a abordagem mais simples que eu poderia pensar e necessário definir novas propriedades Panel.ZIndex anexado para todos os elementos de imagem.

Um atualizador breve: Normalmente quando os filhos de um painel estão sobrepostos, eles são organizados de plano de fundo para primeiro plano com base na sua posição na coleção Children de painel. No entanto, a classe de painel define uma propriedade anexada chamada ZIndex efetivamente substitui o índice de filho. (O nome é gonal de Orto ­ Z para o plano XY convencional da tela, conceitualmente vem fora da tela.) Elementos com um valor mais baixo de ZIndex estão em segundo plano;valores mais altos de ZIndex colocar um elemento de primeiro plano. Se dois ou mais elementos sobrepostos têm a mesma configuração ZIndex (que é o caso, por padrão), seu filho índices na coleção Children são usadas em vez disso, para determinar qual é a do outro.

Programas mais antigos, usei o código a seguir para definir novos valores Panel.ZIndex, onde o elemento de variável é o elemento que está sendo tocado e pnl (do tipo de painel) é o pai do elemento e seus irmãos:

for (int i = 0; i < pnl.Children.Count; i++)
     Panel.SetZIndex(pnl.Children[i],
        pnl.Children[i] == element ?
pnl.Children.Count : i);

Esse código garante que o elemento tocado obtém o mais alto ZIndex e aparece em primeiro plano.

Infelizmente, o elemento tocado saltará para o primeiro plano em um movimento repentino, em vez disso, anormais. Às vezes, outros elementos alternar lugares ao mesmo tempo. (Se você tiver quatro elementos sobrepostos e toque na primeira, ele obtém um ZIndex de 4 e os outros têm valores de ZIndex 1, 2 e 3. Agora, se você toca com a quarta, a primeira volta para um ZIndex igual a 0 e, de repente, irão atrás de todas as outras.)

Meu objetivo era evitar encaixe repentino elementos para o primeiro plano e plano de fundo. Eu queria um efeito mais suave que as do processo de adiamento de uma foto de abaixo de uma pilha e sofrendo adiamentos novamente na parte superior. Em minha mente, comecei a pensar dessas transições como “ Z suave ”. Nada poderia ir para o primeiro ou segundo plano, mas que você moveu um elemento ao redor, eventualmente, ele seria localizar na parte superior de todas as outras. (Uma abordagem alternativa é implementada no controle ScatterView disponível para download no CodePlex de scatterview.codeplex.com/releases/view/24159 . ScatterView é certamente preferível ao lidar com grandes quantidades de itens.)

Na implementação desse algoritmo, posso definir alguns critérios para mim. Em primeiro lugar, não desejo manter informações de estado de um evento de movimentação para o próximo. Em outras palavras, eu não quiser analisar se o elemento manipulated foi interseção outro elemento anteriormente, mas não era mais. Em segundo lugar, eu queria que executam as alocações de memória durante os eventos ManipulationDelta porque pode haver muitos deles. Em terceiro lugar, para evitar muita complexidade, eu queria restringir as alterações da ZIndex relativa apenas o elemento manipulated.

Do Figura 3 mostra o algoritmo completo. Crucial à abordagem é determinar se dois elementos irmãos visualmente se cruzam. Há várias maneiras de fazer isso, mas o código que usei (no método AreElementsIntersecting) pareceu a mais simples. Ele reutiliza dois objetos de RectangleGeometry armazenados como campos.

A Figura 3 T ele algoritmo de suavização Z

// BumpUpZIndex with reusable SortedDictionary object
SortedDictionary<int, UIElement> childrenByZIndex = new 
SortedDictionary<int, UIElement>();

void BumpUpZIndex(FrameworkElement touchedElement, UIElementCollection siblings)
{
  // Make sure everybody has a unique even ZIndex
  for (int childIndex = 0; childIndex < siblings.Count; childIndex++)
  {
        UIElement child = siblings[childIndex];
        int zIndex = Panel.GetZIndex(child);
        Panel.SetZIndex(child, 2 * (zIndex * siblings.Count + childIndex));
  }

  int zIndexNew = Panel.GetZIndex(touchedElement);
  int zIndexCantGoBeyond = Int32.MaxValue;

  // Don't want to jump ahead of any intersecting elements that are on top
  foreach (UIElement child in siblings)
        if (child != touchedElement && 
            AreElementsIntersecting(touchedElement, (FrameworkElement)child))
        {
            int zIndexChild = Panel.GetZIndex(child);

            if (zIndexChild > Panel.GetZIndex(touchedElement))
                zIndexCantGoBeyond = Math.Min(zIndexCantGoBeyond, zIndexChild);
        }

  // But want to be in front of non-intersecting elements
  foreach (UIElement child in siblings)
        if (child != touchedElement && 
            !AreElementsIntersecting(touchedElement, (FrameworkElement)child))
        {
            // This ZIndex is odd, hence unique
            int zIndexNextHigher = 1 + Panel.GetZIndex(child);
            if (zIndexNextHigher < zIndexCantGoBeyond)
                zIndexNew = Math.Max(zIndexNew, zIndexNextHigher);
        }

  // Now give all elements indices from 0 to (siblings.Count - 1)
  Panel.SetZIndex(touchedElement, zIndexNew);
  childrenByZIndex.Clear();
  int index = 0;

  foreach (UIElement child in siblings)
        childrenByZIndex.Add(Panel.GetZIndex(child), child);

  foreach (UIElement child in childrenByZIndex.Values)
        Panel.SetZIndex(child, index++);
    }

// Test if elements are intersecting with reusable //       
RectangleGeometry objects
RectangleGeometry rectGeo1 = new RectangleGeometry();
RectangleGeometry rectGeo2 = new RectangleGeometry();

bool AreElementsIntersecting(FrameworkElement element1, FrameworkElement         
 element2)
{
 rectGeo1.Rect = new 
  Rect(new Size(element1.ActualWidth, element1.ActualHeight));
 rectGeo1.Transform = element1.RenderTransform;

 rectGeo2.Rect = new 
  Rect(new Size(element2.ActualWidth, element2.ActualHeight));
 rectGeo2.Transform = element2.RenderTransform;

 return rectGeo1.FillContainsWithDetail(rectGeo2) != IntersectionDetail.Empty;
}

O método BumpUpZIndex realiza a maior parte do trabalho. Ele começa verificando se que todos os irmãos têm valores exclusivos de ZIndex e que todos esses valores são números pares. O novo ZIndex para o elemento manipulated não pode ser maior do que qualquer valor ZIndex de qualquer elemento que é a interseção e atualmente na parte superior do elemento manipulated. Considerando esse limite, o código tenta atribuir um novo ZIndex que é maior do que os valores de ZIndex de todos os elementos sem interseção.

Normalmente, o código que abordamos até agora terá o efeito de progressivamente ZIndex valores sem limite de aumento, eventualmente, exceder o valor máximo inteiro positivo e se tornar negativo. Essa situação é evitada usando um SortedDictionary. Todos os irmãos são colocados no dicionário com seus valores ZIndex como chaves. Em seguida, os elementos podem ser designados novos valores de ZIndex com base nos seus índices no dicionário.

O algoritmo de suavização Z possui uma sutileza ou dois. Adiadas em relação se o elemento manipulated é interseção elemento A, mas não o elemento B, em seguida, ele não pode ser na parte superior de B se B tem um ZIndex maior que a. Além disso, não houve nenhum acomodação especial para a manipulação de dois ou mais
elementos ao mesmo tempo.

Manipulação sem transformações

Todos os exemplos que mostrei até agora, usei as informações fornecidas com o evento ManipulationDelta para alterar o RenderTransform do elemento manipulated. Não é a única opção. Na verdade, se você não precisa de rotação, você pode implementar manipulação multi-touch sem quaisquer transformações em todos os.

Essa abordagem “ Nenhuma transformação ” envolve o uso de uma tela de desenho como um recipiente para os elementos manipulated. Você pode mover os elementos na tela, definindo a Canvas. Left e Canvas.Top anexado propriedades. Alterar o tamanho dos elementos requer como manipular as propriedades Height e width, com os mesmos valores de escala de percentual usados anteriormente ou com os valores absolutos de expansão.

Uma vantagem distinta dessa abordagem é que podem decorar os elementos manipulated com uma borda se não se tornam maiores e menores conforme você altera o tamanho do elemento.

Essa técnica é demonstrada no projeto NoTransformManipulation, que inclui um derivativo do UserControl NoTransformPictureFrame que implementa a lógica de manipulação de chamada.

Essa classe nova no quadro de imagem não é quase tão sofisticado que o ManipulablePictureFrame. O quadro de imagem anterior usado uma linha pontilhada para um efeito de recorte. Se você aumentar como um quadro para acomodar o maior filho, mas sem aplicar uma transformação, a espessura da linha permanecerá a mesma e o número de pontos em que a linha pontilhada aumentará! Isso parece muito peculiar e provavelmente está desviando minha atenção demais para um programa da vida real. O quadro de imagem no novo arquivo é apenas uma borda simples com cantos arredondados.

No arquivo MainPage.xaml no projeto NoTransformManipulation, cinco objetos NoTransformPictureFrame são organizados na tela de desenho, tudo o que contém elementos de imagem e tudo isso com Canvas. Left exclusivo e Canvas.Top anexado propriedades. Além disso, me deu cada NoTransformPictureFrame uma largura de 200 mas height. Ao redimensionar elementos de imagem, é melhor especificar apenas uma dimensão e permitir que o elemento escolha sua outra dimensão para manter as proporções adequadas.

O arquivo NoTransformPictureFrame.xaml.cs é semelhante na estrutura do código ManipulablePictureFrame, exceto pelo fato de que nenhum código de transformação é necessário. A substituição de OnManipulationDelta ajusta a Canvas. Left e Canvas.Top anexado propriedades e os valores de expansão para aumentar a propriedade Width do elemento. Apenas um pouco de trickiness é obrigatório quando o dimensionamento é na verdade, pois os fatores de conversão precisam ser ajustada para acomodar o centro do dimensionamento.

Uma alteração também foi necessário no método AreElementsIntersecting que desempenha um papel fundamental em transições de Z suaves. O método anterior criados dois objetos de RectangleGeometry refletindo as dimensões não transformadas de dois elementos e, em seguida, aplicada as duas configurações de RenderTransform. O método de substituição é mostrado no do Figura 4. Esses objetos RectangleGeometry sejam baseiam apenas o real tamanho do elemento deslocamento pelo Canvas. Left and Canvas.Top relacionado a propriedades.

Figura 4 de lógica de Z suave alternativa para a manipulação sem transformações

bool AreElementsIntersecting(FrameworkElement element1, FrameworkElement element2)
{
    rectGeo1.Rect = new Rect(Canvas.GetLeft(element1), Canvas.GetTop(element1),
      element1.ActualWidth, element1.ActualHeight);

    rectGeo2.Rect = new Rect(Canvas.GetLeft(element2), Canvas.GetTop(element2),
      element2.ActualWidth, element2.ActualHeight);

    return rectGeo1.FillContainsWithDetail(rectGeo2) != IntersectionDetail.Empty;
}

Problemas restantes

Como eu já discutir os eventos de manipulação, tenha sido ignorando um recurso importante e o elefante na sala tornou maiores. Qui ao recurso é inércia, o que eu irá lidar com a próxima edição.

Charles Petzold é um há muito tempo editor colaborador da MSDN Magazine. Atualmente, ele está gravando “ Programming Windows telefone 7, ” que será publicado como um download e-book gratuito no outono de 2010. Uma edição de visualização está atualmente disponível ­ capaz de por meio de seu site, de charlespetzold.com .

Graças aos seguintes especialistas técnicos para revisão desta coluna: Girbal Doug de e de Robert Levy