terça-feira, 5 de abril de 2011

Exemplo de TextBox com máscara de horário em C#

Olha eu aqui de novo.


Hoje vou escrever um pouco sobre máscaras em TextBox. Mais especificamente sobre uma TextBox com máscara de horário no formato xx:xx que eu precisei fazer, mas a lógica básica para criar uma máscara serve como ideia, fora isso, só é necessário aplicar as regras específicas dependendo das regras da sua máscara.
O post de hoje é um pouco diferente, pois eu não vou, digamos assim, "dar o assunto por encerrado", a ideia é deixar a minha máscara incompleta, ou seja, não aplicar todas as regras necessárias.
Por quê?
Primeiro: porque dependendo de como você trabalha com a TextBox algumas regras podem ser desnecessárias, pois podem nunca acontecer.
Segundo: porque é para ser uma ideia geral apenas e não necessariamente completa.
Terceiro e mais improvável: se alguém ler e perguntar algo ou sugerir um problema relacionado especificamente com a máscara que eu vou abordar no post, eu posso fazer um edit e adicionar o caso sugerido.


Bom, primeiro explicando a ideia básica.
O objetivo é uma TextBox com uma máscara de horário xx:xx que, quando o usuário digitar, vá substituindo do primeiro ao último dígito do horário automaticamente. Ela começará com o valor 00:00 e as regras básicas que eu vou aplicar são:
1 - O primeiro dígito das horas não pode ser maior do que 2.
2 - Se o primeiro dígito das horas for 2 o segundo não pode ser maior do que 3.
3 - O primeiro dígito dos minutos não pode ser maior do que 5.
4 - O usuário não pode substituir o ':' em ocasião alguma.



Vamos supor que eu tenha uma TextBox chamada horario1 no meu WindowsForm.
Ela é instanciada na parte de Design da minha classe então não vou entrar em detalhes dessa parte aqui.
Meu foco é a parte do código na classe Form1.cs.
Vejamos:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Horarios
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            this.horario1.SelectionLength = 0;
            /* A modificação abaixo pode ser feita pela interface, só pus aqui para deixar claro que a TextBox está sendo inicializada dessa forma.*/
            this.horario1.Text = "00:00";
        }

        private void MascaraHorario_Enter(object sender, EventArgs e)
        {
            ((TextBox)sender).SelectionStart = 0;
            ((TextBox)sender).SelectionLength = 1;
        }

        private void MascaraHorario_KeyPress(object sender, KeyPressEventArgs e)
        {
            /*Essa linha serve para impedir o evento de ocorrer como normalmente ocorreria. É como dizer ao compilador que o evento já foi tratado. Isso serve, nesse caso, como uma garantia de que a função de determinadas teclas não seja executada.*/
            e.Handled = true;

            /*Determina o número de caracteres selecionados na TextBox, isso evita que o usuário selecione e substitua mais de um caracter, o que poderia remover a máscara da TextBox.*/
            ((TextBox)sender).SelectionLength = 1;

            /*A propriedade SelectionStart indica a posição do cursor. A partir dela podemos limitar as condições de cada caracter.*/
            switch (((TextBox)sender).SelectionStart)
            {
                case 0:
                    /*Cursor na posição zero. Significa que o caracter a ser substituído é o primeiro.*/
                    switch (e.KeyChar)
                    {
                        /*Limitado para aceitar de 0 a 2.*/
                        case '0':
                        case '1':
                        case '2':
                            /*Substitui o caracter selecionado pelo digitado.*/
                            ((TextBox)sender).SelectedText = e.KeyChar.ToString();
                            /*Uma vez executada essa instrução a propriedade SelectionStart terá sido incrementada, o que significa que o cursor já está em posição para substituir o próximo caracter.*/
                            break;
                        default:
                            break;
                    }
                    break;
                case 1:
                    /*Cursor na posição um. Significa que o caracter a ser substituído é o segundo.*/
                    if (((TextBox)sender).Text[0] == '2')
                        switch (e.KeyChar)
                        {
                            /*No caso de o primeiro caracter ser '2', limitados o segundo entre 0 e 3.*/
                            case '0':
                            case '1':
                            case '2':
                            case '3':
                                ((TextBox)sender).SelectedText = e.KeyChar.ToString();
                                /*Aqui é necessário incrementar a SelectionStart, pois o cursor está em frente ao ':' da máscara, o que faria com que ele fosse o substituído na próxima digitação, e isto não deve acontecer.*/
                                ((TextBox)sender).SelectionStart++;
                                break;
                            default:
                                break;
                        }
                    /*Se o primeiro caracter for diferente de '2' vai para default, que é para inserir caracteres de 0 a 9. Muitas pessoas me trucidariam por causa do 'goto'. Vou escrever um pouco sobre isso depois.*/
                    else goto default;
                    break;
                case 3:
                    switch (e.KeyChar)
                    {
                        /*Limitado para aceitar de 0 a 5.*/
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                            ((TextBox)sender).SelectedText = e.KeyChar.ToString();
                            break;
                        default:
                            break;
                    }
                    break;
                case 5:
                    /*Se o usuário por acaso colocar o cursor na última posição da TextBox, o caracter substituído será o primeiro.*/
                    ((TextBox)sender).SelectionStart = 0;
                    ((TextBox)sender).SelectionLength = 1;
                    goto case 0;
                default:
                    switch (e.KeyChar)
                    {
                        /*Por default a TextBox aceitará caracteres de 0 a 9. Isso acontecerá no segundo caracter, quando ele for diferente de dois, e no quinto caracter. Resolvi deixar essa parte no default, já que outras posições não deverão existir na TextBox com máscara.*/
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            ((TextBox)sender).SelectedText = e.KeyChar.ToString();
                            /*Se a SelectionStart for igual a 2 significa que o cursor está na frente do ':', por isso ela deve ser incrementada, impedindo que o ':' seja substituído. Se ela for igual a 5, significa que chegou no final da máscara, então ela volta para o começo.*/
                            if (((TextBox)sender).SelectionStart == 2)
                                ((TextBox)sender).SelectionStart++;
                            else if (((TextBox)sender).SelectionStart == 5)
                                ((TextBox)sender).SelectionStart = 0;
                            break;
                        default:
                            break;
                    }
                    break;
            }
            /*Aqui é uma questão visual. Pode parecer repetitivo executar essa instrução no início e no fim do evento, mas lá no início ela era uma questão de segurança. Aqui o que ela fará é deixar o próximo caracter selecionado, deixando o usuário saber qual o caracter que ele está prestes a substituir na próxima digitação.*/
            ((TextBox)sender).SelectionLength = 1;
        }

        private void MascaraHorario_KeyDown(object sender KeyEventArgs e)
        {
            e.Handled = true;
        }
    }
}



Bom. Não consigo imaginar muito mais o que explicar.
A maioria das explicações eu preferi colocar no meio do código como comentários.
Depois de feito o código do evento é preciso associar ele ao(s) controle(s). Não vou entrar nesses detalhes porque acredito que qualquer um que tenha chegado ao ponto de ler o post até aqui já sabe como isso funciona.
Uma observação importante é o fato de eu estar usando o object sender do evento.
Esse object sender é uma referência ao controle que disparou o evento.
Usar o sender evita que eu tenha que criar esse evento para várias TextBox, caso eu tenha mais de uma TextBox com máscara. Sendo assim, eu posso criar um único evento e associá-lo a todas as caixas de texto em que eu precise dessa máscara. É um recurso muito útil, mas vale obervar que para utilizá-lo é preciso fazer o casting correto (casting é a conversão que faço quando coloco no código o (TextBox)sender, forçando o sistema a ver o object como TextBox).

Ah sim.
Sobre o evento KeyDown: o evento KeyPress que é o principal para a máscara, não dá acesso a algumas teclas, como o Delete, por exemplo. Para impedir que o usuário use determinadas teclas para remover minha máscara, eu uso a propriedade Handled. Como eu expliquei lá em cima, no código, essa propriedade diz se o evento já foi tratado ou não. Quando você atribui 'true' a essa propriedade o tratamento de evento que deveria acontecer não acontece.

Provavelmente eu precise pedir desculpas a todos os programadores da Terra, pois eu quebrei a regra de evitar a todo custo usar a instrução goto. Pelo que li até hoje essa instrução é um grande tabu em programação.
Mas eu não vou me desculpar. Eu realmente fiquei bem receoso quando essa solução me veio à cabeça. Tentei pensar em uma forma diferente de fazer isso, mas aparentemente essa ideia já tinha tomado conta, ou então essa realmente é a forma de se fazer isso nesse caso.
Eu parei de me preocupar porque até onde eu sei a instrução goto é um tabu porque sendo mal usada ela pode acabar colocando o código em um loop indevido ou até infinito.
No meu caso estou bastante certo de que isso não acontecerá. Apesar das otimizações de código que o compilador .NET faz (e que é algo que me incomodou bastante quando descobri), não acho que ele possa distorcer isso de alguma forma a ponto de criar um loop. Minha intenção foi somente evitar ter de escrever trechos de código iguais em vários lugares.

Essa é a minha TextBox com máscara de hora xx:xx. Na verdade, a minha tem mais alguns recursos.
Mas vou deixar por isso mesmo, porque enquanto não vem ao caso.

Dúvidas ou sugestões, é só postar.

2 comentários:

Thiago Bones disse...

Bom dia, cara, muito bom o tópico...estou fazendo algo semelhante em asp.net usei de JS para criar uma mascara de CNPJ, porém, a textbox que recebe o valor do CNPJ, joga esse vlr em uma variavel que posteriormente eu faça uma consulta em meu banco de dados, é aí que está.
Usando da mascara, ele grava o cnpj assim...11.111.111/1111-11 porém, no meu banco ele não grava ./- como posso proceder nesse caso?

_ivan disse...

Thiago.
Desculpa a demora pra responder, nem sei se ainda é tempo. Por algum motivo não recebi notificação do teu comentário no meu e-mail.

Normalmente quando se trabalha com objetos string tu tens métodos próprios para remover caracteres indesejados, por exemplo o método Trim.
Poderias fazer um Trim para cada um dos caracteres que queres remover antes de criar a query de consulta no teu banco de dados.
Também podes usar o método Replace e substituir teus caracteres por um vazio.
Exemplo pra cada caso:
string cnpjQuery = tuaVariavelCNPJ.Trim('.','/','-');


OU


string cnpjQuery = tuaVariavelCNPJ;
cnpjQuery.Replace(".","");
cnpjQuery.Replace("/","");
cnpjQuery.Replace("-","");





Os nomes das variáveis são ilustrativos.
Não sei os nomes das tuas.
Tu precisas ver também onde estão tuas strings e em que ponto do código tu vai realizar a operação que exemplifiquei.