L’impression multi-page en WPF

Vous avez déjà très certainement eu à imprimer des documents grâce à WPF. Cela se résumant souvent à un PrintDialog et à l’appel sa méthode PrintVisual. Mais cette dernière ne permet qu’une impression sur une seule et même page, si le contenu est trop grand, celui-ci risque d’être tronqué lors de l’impression.

Nous pourrions alors diviser par nous-même le contenu en plusieurs groupes de contrôles et appeler la méthode PrintVisual pour chaque “groupe”. Comme exemple, j’ai pris une ListView contenant 3000 éléments, et sur chaque page, je peux en contenir 45, imaginez que vous deviez imprimer les 67 pages par 67 appels de la méthode PrintVisual et donc autant de tâches d’impressions envoyées à l’imprimante. Ne serais-ce que pour son aspect bricolage, cette méthode est à proscrire.

Heureusement, il nous est possible d’imprimer sur plusieurs pages de manière plus élégante, nous utiliserons toujours une instance de PrintDialog, mais cette fois-ci nous appellerons la méthode PrintDocument, celle-ci a de même deux paramètres, une chaine de caractère représentant la description de l’impression et un objet de type DocumentPaginator. Si nous regardons la documentation sur le MSDN, nous voyons que cette classe est abstraite, nous allons donc créer une nouvelle classe dans notre projet que nous ferrons hériter de DocumentPaginator

public class OneFor4Paginator : DocumentPaginator
{
    public override DocumentPage GetPage(int pageNumber)
    {
        throw new NotImplementedException();
    }
    public override bool IsPageCountValid
    {
        get { throw new NotImplementedException(); }
    }
    public override int PageCount
    {
        get { throw new NotImplementedException(); }
    }
    public override Size PageSize
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }
    public override IDocumentPaginatorSource Source
    {
        get { throw new NotImplementedException(); }
    }
}

Nous verrons l’implémentation détaillée des méthodes et propriétés un plus loin, mais ce qui va surtout nous intéresser ici est la propriété PageCount et la méthode GetPage(…). Comme leurs nom l’indiquent, la propriété PageCount doit retourner le nombre de pages à imprimer et la méthode GetPage retournera un objet de type DocumentPage qui représentera physiquement une page imprimée. Un petit coup d’œil sur la documentation MSDN nous montre que le constructeur de DocumentPage prend en paramètre un Visual. En regardant le diagramme de classe, on voit que UIElement hérite de Visual,  nous allons donc ici simplement utiliser un UserControl pour représenter le visuel de notre DocumentPage, celui ci contiendra uniquement une GridView avec deux colonnes dans notre cas, l’extrait de code suivant montre le conteneur parent de notre UserControl.

<Grid>
   <ListView Margin= »50″ x:Name= »liste » Grid.Row= »1″ ItemsSource= »{Binding Path=ListeItem, ElementName=Page} » ScrollViewer.VerticalScrollBarVisibility= »Hidden »>
      <ListView.View>
         <GridView>
            <GridViewColumn Header= »Id » DisplayMemberBinding= »{Binding Id} » Width= »50″ />
            <GridViewColumn Header= »Nom » DisplayMemberBinding= »{Binding Nom} » Width= »200″ />
         </GridView>
      </ListView.View>
   </ListView>
</Grid>
 

Chaque page sera totalement indépendante et recevra à sa construction les seuls éléments qu’elle doit imprimer. Le code-behind de notre UserControl sera donc le suivant :

public partial class OneFor4Page : UserControl, INotifyPropertyChanged
{
    private const double PageMargin = 15;
    private const double ElementHeight = 24;
    private IEnumerable<ModelOneFor4> _listeItem;
    public IEnumerable<ModelOneFor4> ListeItem
    {
        get { return _listeItem; }
        private set
        {
            _listeItem = value;
            this.RaisePropertyChanged(« ListeItem »);
        }
    }
    public OneFor4Page(IEnumerable<ModelOneFor4> lstItems)
    {
        ListeItems = lstItems;
        this.Margin = new Thickness(PageMargin);
        InitializeComponent();
    }
    public static int RowPerPage(double height)
    {
        return (int)Math.Floor((height – (2 * PageMargin)) / ElementHeight);
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

 

La propriété constante ElementHeight nous servira ici dans la méthode statique RowPerPage retournant le nombre d’éléments que peut contenir une page, par soucis de simplicité, nous divisons juste la hauteur disponible par la hauteur d’un élément pour obtenir ce nombre.

A présent que nous avons notre “page” retournons à notre OneFor4Paginator. Nous commençons par ajouter deux champs à notre classes, _pageSize de type System.Windows.Size et _rowsPerPage de type int. A chaque modification de la propriété PageSize, nous mettons à jour _pageSize et nous appelons la méthode de classe RowPerPage pour mettre de même à jour notre champ _rowsPerPage. Nous nous occupons ensuite de remplir les propriétés IsPageCountValid et PageCount, dans cet article, nous retournerons toujours la valeur true pour la validation du nombre de pages, quant aux nombres de pages, nous retournons simplement le nombre d’éléments à imprimer par le nombre de d’éléments par page ( vous suivez jusque là Sourire ).

Quant à la méthode GetPage, nous récupérons la ligne par laquelle nous devons commencer à imprimer grâce au numéro de page passé en paramètre ( plus par pratique que par logique, ce numéro démarre à 0 ), nous calculons le nombres d’éléments à imprimer en prenons soin de vérifier s’il reste assez d’éléments pour remplir une page et éviter une belle “ArgumentException”, puis nous appelons le constructeur de notre page. Avant de passer notre UserControl en paramètre du DocumentPage à retourner, il nous faut appeller successivement les méthodes du framework Measure et Arrange sur notre UserControl, celle-ci s’occuperons de placer tout le contenu de notre UserControl, sans elles, nous imprimerons des pages entièrement blanches…

class OneFor4Paginator : DocumentPaginator
{
    private Size _pageSize;
    IEnumerable<ModelOneFor4> _listeItems;
    private int _rowPerPage;
    public OneFor4Paginator(Size pageSize, IEnumerable<ModelOneFor4> listeItems)
    {
        this._listeItems = listeItems;
        this.PageSize = pageSize;
    }
    public override DocumentPage GetPage(int pageNumber)
    {
        int currentRow = _rowPerPage * pageNumber;
        OneFor4Page page;
        int printableRowCount = Math.Min(_rowPerPage, _listeItems.Count() – currentRow);
        page = new OneFor4Page(_listeItems.ToList().GetRange(currentRow, printableRowCount));
        page.Measure(PageSize);
        page.Arrange(new Rect(new Point(0, 0), PageSize));
        return new DocumentPage(page);
    }
    public override bool IsPageCountValid
    {
        get { return true; }
    }
    public override int PageCount
    {
        get { return (int)Math.Ceiling(_listeItems.Count() / (double)_rowPerPage); }
    }
    public override System.Windows.Size PageSize
    {
        get
        {
            return _pageSize;
        }
        set
        {
            _pageSize = value;
            _rowPerPage = OneFor4Page.RowPerPage(PageSize.Height);
        }
    }
    public override IDocumentPaginatorSource Source
    {
        get { return null; }
    }
}

Il ne nous reste plus qu’à instancier un PrintDialog et appeller la méthode PrintDocument avec en paramètre un OneFor4Paginator créée précédemment.

PrintDialog dialog = new PrintDialog();
bool? dialogResult = dialog.ShowDialog();
if (dialogResult.HasValue && dialogResult.Value)
{
    dialog.PrintDocument(new OneFor4Paginator(new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight), _listeItems), « Impression OneFor4 »);
}

 

Voilà donc une façon simple d’imprimer des documents de plusieurs pages grâce à WPF, par souci de rapidité, nous avons créé notre page en code XAML, ce qui a comme “défaut” d’imprimer quelque chose ressemblant à de simples impression écran de notre logiciel. Mais nous aurions pu tout aussi bien ré implémenter la méthode OnRender de notre UserControl et “dessiner” notre page à la main grâce au DrawingContext, avec un peu d’habitude et de la refactorisation de code, cette manière de faire peut s’avérer tout aussi rapide que la création en code xaml tout en imprimant un “vrai” document sans la sensation d’impression écran…

Aucun arbre n’a du être coupé pour tester le code de cet article, par respect de la nature, utilisez au maximum les imprimantes virtuelles pour vos tests. Microsoft XPS Document Writer ne donne certes pas toujours des aperçus réalistes mais d’autres logiciels comme PDF Créator nous permettent d’obtenir un aperçu très proche des vrais impressions.

Les recherches pour cet article ont été faites dans le cadre d’un stage pour l’entreprise SACEO et son logiciel Opisto : www.opisto.fr

Par Mathieu Hollebecq

5 réponses à “L’impression multi-page en WPF

  1. Qu’est-ce que ModelOneFor4?!?

  2. Mathieu Hollebecq

    C’est une simple classe de modele de donnée que j’ai créée avec un champ « Id » et un champ « Nom »:

    public class ModelOneFor4
    {
    public int Id {get;set;}
    public String Nom {get;set;}
    }

  3. Bonjour
    Le binding ElementName=Page ne fait référene à rien ?
    Erreur dans le constructeur OneFor4Page ListeItems ne doit pas prendre de s la propriété à initialiser étant ListeItem.
    J’ai rectifié le constructeur et éliminé dans le binding la mention ElementtName=Page, mais je n’imprime que les Headers, rien de la ListeItem dont le contenu est bien présent dans la variable page. ??
    Bien entendu j’ai fourni dans l’appel PrintDocument la liste d’Items dans le format adéquat.
    Merci si vous pouvez m’éclairer

  4. Finalement j’ai trouvé : il manque après : page. Measure(PageSize) la déclaration page.UpdateLayout();
    Aprés l’avoir ajouté c’est OK

  5. Rajouté page.UpDateLayout() et c’est OK
    Manquait aussi l’appel dans le constructeur à DataContext

Répondre à Mathieu Hollebecq Annuler la réponse.