1108 lines
39 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Resume
DateNoDotConverter.cs:
```cs
//BackgroundBuilder\Converters\DateNoDotConverter.cs
using System;
using System.Globalization;
using System.Windows.Data;
namespace BackgroundBuilder.Converters
{
public class DateNoDotConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime dt)
{
// get "MMM" then TrimEnd('.'):
string month = culture
.DateTimeFormat
.GetAbbreviatedMonthName(dt.Month)
.TrimEnd('.');
return $"{dt:dd}/{month}";
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
```
Contato.cs:
```cs
//BackgroundBuilder\Models\Contato.cs
using System;
namespace BackgroundBuilder.Models
{
public class Contato
{
public string Ramal { get; set; } = string.Empty;
public string? Nome { get; set; } = string.Empty;
public string? Email { get; set; }
public string? Area { get; set; }
public DateTime? Aniversario { get; set; }
public bool IsComando { get; set; }
}
}
```
IContatoRepository.cs:
```cs
//BackgroundBuilder\Repositories\IContatoRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using BackgroundBuilder.Models;
namespace BackgroundBuilder.Repositories
{
public interface IContatoRepository
{
Task<IEnumerable<Contato>> GetAllAsync();
Task InsertUpdateAsync(Contato c);
Task DeleteAsync(string ramal);
}
}
```
PostgresContatoRepository.cs:
```cs
//BackgroundBuilder\Repositories\PostgresContatoRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapper;
using Npgsql;
using BackgroundBuilder.Models;
using BackgroundBuilder.Services;
namespace BackgroundBuilder.Repositories
{
public class PostgresContatoRepository(DatabaseService db) : IContatoRepository
{
private readonly DatabaseService _db = db;
public async Task<IEnumerable<Contato>> GetAllAsync()
{
using var conn = _db.CreateConnection();
const string sql = @"
SELECT
ramal AS Ramal,
nome AS Nome,
email AS Email,
area AS Area,
aniversario AS Aniversario,
""isComando"" AS IsComando
FROM contatos;";
return await conn.QueryAsync<Contato>(sql);
}
public async Task AddAsync(Contato c)
{
using var conn = _db.CreateConnection();
const string sql = @"
INSERT INTO contatos
(ramal, nome, email, area, aniversario, ""isComando"")
VALUES
(@Ramal, @Nome, @Email, @Area, @Aniversario, @IsComando);";
await conn.ExecuteAsync(sql, c);
}
public async Task DeleteAsync(string ramal)
{
using var conn = _db.CreateConnection();
const string sql = "DELETE FROM contatos WHERE ramal=@Ramal;";
await conn.ExecuteAsync(sql, new { Ramal = ramal });
}
public async Task InsertUpdateAsync(Contato c)
{
using var conn = _db.CreateConnection();
string Ramal = $"'{c.Ramal}'";
string Nome = c.Nome is null ? "null" : $"'{c.Nome}'";
string Email = c.Email is null ? "null" : $"'{c.Email}'";
string Area = c.Area == "" ? "null" : $"'{c.Area}'";
string Aniversario = c.Aniversario is null ? "null" : $"'{c.Aniversario:yyyy-MM-dd}'";
string IsComando = c.IsComando ? "true" : "false";
string sql = @$"
INSERT INTO contatos
(ramal, nome, email, area, aniversario, ""isComando"")
VALUES
({Ramal}, {Nome}, {Email}, {Area}, {Aniversario:YYYY-MM-DD}, {IsComando})
ON CONFLICT (ramal) DO UPDATE SET
nome = EXCLUDED.nome,
email = EXCLUDED.email,
area = EXCLUDED.area,
aniversario = EXCLUDED.aniversario,
""isComando"" = EXCLUDED.""isComando"";";
await conn.ExecuteAsync(sql);
}
}
}
```
DatabaseService.cs:
```cs
//BackgroundBuilder\Services\DatabaseService.cs
using System;
using Microsoft.Extensions.Configuration;
using Npgsql;
namespace BackgroundBuilder.Services
{
public class DatabaseService(IConfiguration config)
{
private readonly string _connString = config.GetConnectionString("ContatosDb")
?? throw new InvalidOperationException("Missing connection string 'ContatosDb'.");
public NpgsqlConnection CreateConnection()
{
var conn = new NpgsqlConnection(_connString);
conn.Open();
return conn;
}
}
}
```
IImageService.cs:
```cs
//BackgroundBuilder\Services\IImageService.cs
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
namespace BackgroundBuilder.Services
{
public interface IImageService
{
/// <summary>
/// Loads an image from disk.
/// </summary>
Task<BitmapImage> LoadAsync(string path);
/// <summary>
/// Renders the background + overlay and writes two PNGs:
/// • primaryPath: composite of background+overlay
/// • overlayPath (optional): overlay alone
/// Returns the actual paths written.
/// </summary>
Task<(string primaryPath, string? overlayPath)> SaveAsync(
FrameworkElement overlay,
BitmapImage background,
string primaryPath,
string? overlayPath = null);
}
}
```
ImageService.cs:
```cs
//BackgroundBuilder\Services\ImageService.cs
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace BackgroundBuilder.Services
{
public class ImageService : IImageService
{
// no longer injected — use a private constant
private static readonly Thickness OverlayOffset = new(10, 58, 10, 58);
public async Task<BitmapImage> LoadAsync(string path)
{
// Load image from disk into BitmapImage
var bmp = new BitmapImage();
using var stream = File.OpenRead(path);
bmp.BeginInit();
bmp.UriSource = new Uri(path, UriKind.Absolute);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
// no real async work—return completed task
return await Task.FromResult(bmp);
}
public async Task<(string primaryPath, string? overlayPath)> SaveAsync(
FrameworkElement overlay,
BitmapImage background,
string primaryPath,
string? overlayPath = null)
{
var compositeBmp = RenderComposite(overlay, background, OverlayOffset);
SaveBitmap(compositeBmp, primaryPath);
string? savedOverlayPath = null;
if (!string.IsNullOrWhiteSpace(overlayPath))
{
var overlayBmp = RenderComposite(overlay, null, new Thickness(0));
SaveBitmap(overlayBmp, overlayPath!);
savedOverlayPath = overlayPath;
}
return await Task.FromResult((primaryPath, savedOverlayPath));
}
/// <summary>
/// Renders a <paramref name="background"/> plus the <paramref name="mainGrid"/>
/// at bottomright offset by <paramref name="offset"/>. If background is null,
/// only the mainGrid is rendered at (0,0).
/// </summary>
private static RenderTargetBitmap RenderComposite(
FrameworkElement mainGrid,
BitmapImage? background,
Thickness offset)
{
// Determine canvas size
int width = background?.PixelWidth ?? (int)mainGrid.ActualWidth;
int height = background?.PixelHeight ?? (int)mainGrid.ActualHeight;
var dv = new DrawingVisual();
using (var ctx = dv.RenderOpen())
{
// Draw background if provided
if (background != null)
{
ctx.DrawImage(
background,
new Rect(0, 0, width, height));
}
// Measure & arrange the mainGrid so ActualWidth/Height are valid
mainGrid.Measure(new Size(width, height));
mainGrid.Arrange(new Rect(0, 0, mainGrid.DesiredSize.Width, mainGrid.DesiredSize.Height));
double ow = mainGrid.ActualWidth > 0 ? mainGrid.ActualWidth : mainGrid.DesiredSize.Width;
double oh = mainGrid.ActualHeight > 0 ? mainGrid.ActualHeight : mainGrid.DesiredSize.Height;
double x = width - ow - offset.Left;
double y = height - oh - offset.Right;
// Draw mainGrid at either origin or bottom-right with offset
var brush = new VisualBrush(mainGrid);
ctx.DrawRectangle(brush, null, new Rect(x, y, ow, oh));
}
var rtb = new RenderTargetBitmap(
width,
height,
96,
96,
PixelFormats.Pbgra32);
rtb.Render(dv);
return rtb;
}
/// <summary>
/// Encodes the given bitmap as PNG and writes it to disk.
/// </summary>
private static void SaveBitmap(RenderTargetBitmap bitmap, string path)
{
// Ensure directory exists
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(fs);
}
}
}
```
ITaskbarService.cs:
```cs
//BackgroundBuilder\Services\ITaskbarService.cs
namespace BackgroundBuilder.Services
{
///
/// Defines methods for querying the Windows taskbar size and orientation.
///
public interface ITaskbarService
{
/// Gets the current thickness (height if horizontal, width if vertical) of the taskbar in pixels.
int GetTaskbarSize();
/// <summary>Returns true when the taskbar is horizontal, false when vertical.</summary>
bool IsHorizontal();
}
}
```
TaskbarService.cs:
```cs
//BackgroundBuilder\Services\TaskbarService.cs
using System;
using System.Runtime.InteropServices;
namespace BackgroundBuilder.Services
{
///
/// Implementation of using P/Invoke (SHAppBarMessage).
///
public class TaskbarService : ITaskbarService
{
#region WinAPI
[DllImport("shell32.dll")]
private static extern IntPtr SHAppBarMessage(uint msg, ref APPBARDATA data);
[StructLayout(LayoutKind.Sequential)]
private struct APPBARDATA
{
public uint cbSize;
public IntPtr hWnd;
public uint uCallbackMessage;
public uint uEdge;
public RECT rc;
public IntPtr lParam;
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int left, top, right, bottom; }
private const uint ABM_GETTASKBARPOS = 5;
#endregion
public int GetTaskbarSize()
{
var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf<APPBARDATA>() };
if (SHAppBarMessage(ABM_GETTASKBARPOS, ref data) == IntPtr.Zero)
throw new InvalidOperationException("Failed to retrieve taskbar position.");
int thickness = IsHorizontal()
? Math.Abs(data.rc.bottom - data.rc.top)
: Math.Abs(data.rc.right - data.rc.left);
return thickness;
}
public bool IsHorizontal()
{
var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf<APPBARDATA>() };
SHAppBarMessage(ABM_GETTASKBARPOS, ref data);
int height = Math.Abs(data.rc.bottom - data.rc.top);
int width = Math.Abs(data.rc.right - data.rc.left);
return width > height;
}
}
}
```
ObservableObject.cs:
```cs
//BackgroundBuilder\Utils\ObservableObject.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace BackgroundBuilder.Utils
{
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
```
RelayCommand.cs:
```cs
//BackgroundBuilder\Utils\RelayCommand.cs
using System;
using System.Windows.Input;
namespace BackgroundBuilder.Utils
{
public class RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null) : ICommand
{
private readonly Action<object?> _execute = execute;
private readonly Func<object?, bool>? _canExecute = canExecute;
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
```
MainWindowViewModel.cs:
```cs
//BackgroundBuilder\ViewModels\MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using BackgroundBuilder.Models;
using BackgroundBuilder.Repositories;
using BackgroundBuilder.Services;
using BackgroundBuilder.Utils;
using Microsoft.Win32;
namespace BackgroundBuilder.ViewModels
{
public class MainWindowViewModel : ObservableObject
{
private readonly IContatoRepository _repo;
private readonly IImageService _imageService;
private readonly ITaskbarService _taskbarService;
public ObservableCollection<Contato> RawContatos { get; } = [];
public ObservableCollection<Contato> Comando { get; } = [];
public ObservableCollection<Contato> ContatosSemCMD { get; } = [];
public ObservableCollection<Contato> Aniversarios { get; } = [];
private Contato? _selectedContato;
public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } }
private ObservableCollection<Contato>? _selectedContatos;
public ObservableCollection<Contato> SelectedContatos { get => _selectedContatos ?? RawContatos; set { _selectedContatos = value; OnPropertyChanged(); UpdateCommand.RaiseCanExecuteChanged(); } }
private string? _caminhoBGImage;
public string? CaminhoBGImage{ get => _caminhoBGImage; set { _caminhoBGImage = value; OnPropertyChanged(); }}
private BitmapImage? _backgroundImage;
public BitmapImage? BackgroundImage{ get => _backgroundImage; private set { _backgroundImage = value; OnPropertyChanged(); }}
private int _taskbarSize;
public int TaskbarSize{ get => _taskbarSize; private set { _taskbarSize = value; OnPropertyChanged(); }}
private string _orientation = "Horizontal";
public string Orientation{ get => _orientation; private set { _orientation = value; OnPropertyChanged(); }}
public FrameworkElement? OverlayElement { get; set; }
// Commands
public RelayCommand DeleteCommand { get; }
public RelayCommand AddCommand { get; }
public RelayCommand SelectImageCommand { get; }
public ICommand RefreshCommand { get; }
public RelayCommand UpdateCommand { get; }
public RelayCommand ExportImageCommand { get; }
public MainWindowViewModel(
IContatoRepository repo,
IImageService imageService,
ITaskbarService taskbarService)
{
_repo = repo;
_imageService = imageService;
_taskbarService = taskbarService;
RefreshTaskbarInfo();
DeleteCommand = new RelayCommand(async _ => await DeleteAsync(), _ => SelectedContato != null);
AddCommand = new RelayCommand(async _ => await AddContactAsync());
SelectImageCommand = new RelayCommand(async _ => await SelectBackground());
RefreshCommand = new RelayCommand(async _ => await LoadRawAsync());
UpdateCommand = new RelayCommand(async _ => await UpdateAsync(), _ => SelectedContatos != null);
ExportImageCommand = new RelayCommand(async _ => await RenderImageAsync(), _ => BackgroundImage != null && OverlayElement != null);
}
public void RefreshTaskbarInfo()
{
TaskbarSize = _taskbarService.GetTaskbarSize();
Orientation = _taskbarService.IsHorizontal() ? "Horizontal" : "Vertical";
}
private async Task DeleteAsync()
{
if (SelectedContato == null) return;
var result = MessageBox.Show(
"Delete selected record?",
"Confirm Delete",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
try
{
await _repo.DeleteAsync(SelectedContato.Ramal);
RawContatos.Remove(SelectedContato);
ApplyFilters();
}
catch
{
MessageBox.Show(
"Error deleting record.",
"Delete Failed",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
private async Task AddContactAsync()
{
var novo = new Contato { Ramal = "", Nome = "" };
RawContatos.Add(novo);
SelectedContato = novo; // Set the newly added contact as the selected one
await Task.CompletedTask; // preserve async signature
}
private async Task SelectBackground()
{
var dlg = new OpenFileDialog
{
Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp"
};
if (dlg.ShowDialog() == true)
{
CaminhoBGImage = dlg.FileName;
BackgroundImage = await _imageService.LoadAsync(dlg.FileName);
if (!string.IsNullOrEmpty(CaminhoBGImage))
{
ExportImageCommand.RaiseCanExecuteChanged();
}
}
}
public async Task LoadRawAsync()
{
var all = await _repo.GetAllAsync();
RawContatos.Clear();
foreach (var c in all.Where(x => x.IsComando)) RawContatos.Add(c);
foreach (var c in all.Where(x => !x.IsComando).OrderBy(x => x.Nome)) RawContatos.Add(c);
ApplyFilters();
}
private async Task RenderImageAsync()
{
var dlg = new SaveFileDialog
{
Filter = "PNG Image|*.png"
};
if (dlg.ShowDialog() == true
&& OverlayElement is FrameworkElement overlay
&& BackgroundImage is BitmapImage bg)
{
await _imageService.SaveAsync(overlay, bg, dlg.FileName, dlg.FileName.Replace(".", "_1."));
}
}
public async Task UpdateAsync()
{
if (SelectedContatos == null) return;
var result = MessageBox.Show(
"Save selected records?",
"Confirm Save",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
foreach (var contato in SelectedContatos)
{
if (string.IsNullOrWhiteSpace(contato.Ramal)
|| string.IsNullOrWhiteSpace(contato.Nome))
{
MessageBox.Show(
"Ramal and Nome are required.",
"Validation Error",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
try
{
await _repo.InsertUpdateAsync(contato);
}
catch
{
MessageBox.Show(
"Error saving record.",
"Save Failed",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
await LoadRawAsync();
MessageBox.Show(
"Concluído!",
"Confirm Save",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
private void ApplyFilters()
{
Comando.Clear();
ContatosSemCMD.Clear();
Aniversarios.Clear();
foreach (var item in RawContatos.Where(x => x.IsComando))
Comando.Add(item);
foreach (var item in RawContatos.Where(x => !x.IsComando).OrderBy(x => x.Nome))
ContatosSemCMD.Add(item);
foreach (var item in RawContatos.Where(x => !x.IsComando).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Day).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Month))
Aniversarios.Add(item);
}
}
}
```
MainWindow.xaml
```xml
<!--BackgroundBuilder\Views\MainWindow.xaml-->
<Window x:Class="BackgroundBuilder.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:BackgroundBuilder.Converters"
Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1230">
<Window.Resources>
<converters:DateNoDotConverter x:Key="DateNoDotConverter" />
</Window.Resources>
<Grid x:Name="RootGrid" Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Row 0: Background selection -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Row 2: Image-->
<Rectangle Grid.Column="0"
Fill="Gainsboro"
Height="90"
Width="160"
>
</Rectangle>
<Image Source="{Binding BackgroundImage}" Height="90" Stretch="Uniform" Grid.Column="0"/>
<Button Content="Selecionar Imagem de Fundo…"
Command="{Binding SelectImageCommand}"
Grid.Column="1"
Padding="20,3,20,3"
Height="30"
Margin="5"/>
<TextBox Text="{Binding CaminhoBGImage}"
Grid.Column="2"
IsReadOnly="True"
Padding="20,3,20,3"
Height="30"
Margin="5"/>
</Grid>
<!-- Row 1: three side-by-side DataGrids -->
<Grid Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" x:Name="MainGrid" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Comando -->
<DataGrid Grid.Column="0"
ItemsSource="{Binding Comando}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Ação" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
</DataGrid.Columns>
</DataGrid>
<!-- ContatosSemCMD -->
<DataGrid Grid.Column="1"
ItemsSource="{Binding ContatosSemCMD}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
<DataGridTextColumn Header="Área" Binding="{Binding Area}"/>
</DataGrid.Columns>
</DataGrid>
<!-- Aniversarios -->
<DataGrid Grid.Column="2"
ItemsSource="{Binding Aniversarios}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Aniversário"
Binding="{Binding Aniversario,Converter={StaticResource DateNoDotConverter}}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Row 3: DataGrid for RawContatos -->
<DataGrid x:Name="RawGrid"
Grid.Column="2"
ItemsSource="{Binding RawContatos}"
SelectedItem="{Binding SelectedContato}"
AutoGenerateColumns="False"
IsReadOnly="False"
IsHitTestVisible ="True"
FontSize="11"
VerticalAlignment="Top"
HorizontalAlignment="Center"
Height="800"
AlternatingRowBackground="DarkGray">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="Gray"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<!-- your existing columns here -->
<DataGrid.Columns>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="Nome" Binding="{Binding Nome, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="Email" Binding="{Binding Email, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="Área" Binding="{Binding Area, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTemplateColumn Header="Aniversário">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding Aniversario, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridCheckBoxColumn Header="É Comando?" Binding="{Binding IsComando}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Row 3: Buttons for actions -->
<Grid Grid.Row="3"
Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Content="Recarregar Contatos"
Command="{Binding RefreshCommand}"
Grid.Column="0"
FontWeight="Medium"
Padding="5"
Margin="5"/>
<Button Content="+ Nova linha"
Command="{Binding AddCommand}"
Grid.Column = "1"
Foreground="Green"
FontWeight="Medium"
Padding="5"
Margin="5"/>
<Button Content="- Deletar selecionada"
Command="{Binding DeleteCommand}"
Grid.Column = "2"
Foreground="Red"
FontWeight="Medium"
Padding="5"
Margin="5"/>
<Button Content="Salvar dados"
Command="{Binding UpdateCommand}"
CommandParameter="RawGrid"
Grid.Column = "3"
FontWeight="Medium"
Padding="5"
Margin="5"/>
<Button Content="Criar Imagem -->"
Command="{Binding ExportImageCommand}"
CommandParameter="MainGrid"
Grid.Column = "4"
Foreground="Blue"
FontWeight="Bold"
Padding="5"
Margin="5"/>
</Grid>
</Grid>
</Window>
```
MainWindow.xaml.cs:
```cs
//BackgroundBuilder\Views\MainWindow.xaml.cs
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using BackgroundBuilder.Models;
using BackgroundBuilder.ViewModels;
namespace BackgroundBuilder.Views
{
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _vm;
public MainWindow(MainWindowViewModel vm)
{
InitializeComponent();
this.Language = System.Windows.Markup.XmlLanguage.GetLanguage(System.Globalization.CultureInfo.CurrentUICulture.IetfLanguageTag);
_vm = vm;
DataContext = vm;
// Point the VMs OverlayElement to our DataGrid
_vm.OverlayElement = MainGrid;
// Load contatos on window load
Loaded += async (_, __) => await _vm.LoadRawAsync();
}
}
}
```
App.xaml
```xml
<!--BackgroundBuilder\App.xaml-->
<Application x:Class="BackgroundBuilder.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="OnStartup">
<Application.Resources>
<Style TargetType="DataGrid">
<Setter Property="AutoGenerateColumns" Value="True"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="CanUserDeleteRows" Value="False"/>
<Setter Property="CanUserReorderColumns" Value="False"/>
<Setter Property="CanUserResizeColumns" Value="False"/>
<Setter Property="CanUserSortColumns" Value="False"/>
<Setter Property="GridLinesVisibility" Value="None"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="RowBackground" Value="GhostWhite"/>
<Setter Property="AlternatingRowBackground" Value="#387f79"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontStyle" Value="Italic" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="IsHitTestVisible" Value="False"/>
</Style>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="#387f79"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Margin" Value="10,0,10,0"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
<Style TargetType="DataGridRow">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="FontSize" Value="16"/>
</Style>
</Application.Resources>
</Application>
```
App.xaml.cs:
```cs
//BackgroundBuilder\App.xaml.cs
using System;
using System.Windows;
using BackgroundBuilder.Repositories;
using BackgroundBuilder.Services;
using BackgroundBuilder.ViewModels;
using BackgroundBuilder.Views;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundBuilder
{
public partial class App : Application
{
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration(cfg =>
cfg.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true))
.ConfigureServices((_, services) =>
{
// Database & repository
services.AddSingleton<DatabaseService>();
services.AddScoped<IContatoRepository, PostgresContatoRepository>();
// New image service
services.AddSingleton<IImageService, ImageService>();
// New taskbar service
services.AddSingleton<ITaskbarService, TaskbarService>();
// VM & View
services.AddTransient<MainWindowViewModel>();
services.AddTransient<MainWindow>();
})
.Build();
}
private async void OnStartup(object sender, StartupEventArgs e)
{
await _host.StartAsync();
var window = _host.Services.GetRequiredService<MainWindow>();
window.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
using (_host) { await _host.StopAsync(); }
base.OnExit(e);
}
}
}
```
appsettings.json:
```json
//BackgroundBuilder\appsettings.json
{
"ConnectionStrings": {
"ContatosDb": "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia"
}
}
```
README.md:
```md
# BackgroundBuilder
[![.NET](https://img.shields.io/badge/.NET-8.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) [![WPF](https://img.shields.io/badge/WPF-%23C8C8C8?logo=windows&logoColor=blue)](https://learn.microsoft.com/dotnet/desktop/wpf/) [![XAML](https://img.shields.io/badge/XAML-0C54C2?logo=xaml&logoColor=white)](https://learn.microsoft.com/dotnet/desktop/wpf/xaml/) [![C#](https://img.shields.io/badge/C%23-239120?logo=c-sharp&logoColor=white)](https://learn.microsoft.com/dotnet/csharp/) [![MVVM](https://img.shields.io/badge/Pattern-MVVM-ff69b4)](https://learn.microsoft.com/pt-br/dotnet/architecture/maui/mvvm) [![DI](https://img.shields.io/badge/DI-Microsoft.Extensions.Hosting-0078D7?logo=azure-devops&logoColor=white)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) [![PostgreSQL](https://img.shields.io/badge/DB-PostgreSQL-4169E1?logo=postgresql&logoColor=white)](https://www.postgresql.org/) [![Npgsql](https://img.shields.io/badge/Driver-Npgsql-008bb9?logo=postgresql&logoColor=white)](https://www.npgsql.org/) [![Dapper](https://img.shields.io/badge/ORM-Dapper-0089D6)](https://github.com/DapperLib/Dapper) [![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows&logoColor=white)](https://www.microsoft.com/windows)
## 📝 Project description
> 🖥️ An MVVM WPF application (.NET 8) providing an Excel-like editor for the `contatos` table in PostgreSQL
---
## 📑 Prerequisites
- [![.NET](https://img.shields.io/badge/.NET_8.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/)
- [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?logo=postgresql&logoColor=white)](https://www.postgresql.org/) **database** with table:
```sql
CREATE TABLE public.contatos (
ramal text PRIMARY KEY NOT NULL,
nome text NOT NULL,
email text,
area text,
aniversario date,
"isComando" boolean NOT NULL
);
```
---
## 🔧 Setup
1. ✏️ Edit `appsettings.json`, set your `ConnectionStrings:ContatosDb`.
2. 🖥️ In a terminal:
```bash
dotnet restore
dotnet build
dotnet run --project BackgroundBuilder.csproj
```
3. 🪟 The main window will appear; on load it fetches and displays all contatos.
---
## 🏗️ Architecture
- 🏛️ **MVVM** with `ObservableObject`, `RelayCommand`
- 🧩 **DI** via `Microsoft.Extensions.Hosting`
- 💾 **Repositories** (`PostgresContatoRepository`) handle all DB I/O with Dapper
- 🔌 **Services** (`DatabaseService`) manage the Npgsql connection
- 🧠 **ViewModels** free of data-access logic: only orchestration
---
## 🚀 Releases
### 📆 21/05/2025: 🆕 Background & Export Features
- 🎨 **Select Background…**
Opens a file picker—choose any image (PNG, JPG, BMP). That image becomes your canvas.
- 🖼️ **Create Image…**
Saves the current DataGrid overlaid on the background as a single PNG.
Uses WPFs `RenderTargetBitmap` and `PngBitmapEncoder` under the hood.
---