Xis’ blog - Mój wirtualny schowek
Polska Społeczność Symfony

Szybkie losowanie rekordów w SQL – jeszcze jeden sposób

Jeśli masz tabelę, w której znajduje się wiele rekordów i stajesz przed potrzebą wyświetlenie kilku losowych rekordów – na ogół masz problem związany z wydajnym sposobem realizacji zapytania.
Są różne sposoby na rozwiązanie tego problemu. Najprostszy jest oczywiście ORDER BY RAND(), jednak słynie on z tego, że jest straszliwie powolny, zwłaszcza na dużych tabelach.
Inne rozwiązanie to załadowanie listy kluczy podstawowych tabeli do tablicy w pamięci i losowanie po stronie programu. Wykonujemy wprawdzie dwa zapytania zamiast jednego, jednak jest to metoda dużo bardziej wydajna w niektórych przypadkach.
Wymyśliłem dziś inny sposób, dzięki któremu nie musimy skanować całej tabeli w celu wyświetlenia losowych rekordów. Rozwiązanie to jest bardzo proste w realizacji.

Wystarczy, że do tabeli, z której chcemy pobierać rekordy dodamy kolumnę np. random_num, najlepiej wesprzeć ją indeksem.  W kolumnie tej zapisywać będziemy losową liczbę z jakiegoś zakresu (np. 1 do 1000) podczas każdego zapisania obiektu.

Gdy mamy już tabelę wypełnioną  rekordami z losowymi wartościami  w kolumnie random_num (wartość ta oczywiście będzie się powtarzać w przypadku, gdy tabela ma > 1000 rekordów, ale to nie szkodzi), wystarczy wykonać dwa zapytania:

SELECT COUNT(*) AS cnt FROM tabela

… by zliczyć ilość rekordów.  Wynik zapytania załadujmy do zmiennej cnt. W zmiennej limit natomiast zapamiętajmy ilość rekordów, jakie chcemy pobrać. Następnie:

SELECT * FROM tabela ORDER BY random_num OFFSET :offset LIMIT :LIMIT

Pod parametr :offset podstawiamy losową wartość (losowaną po stronie programu) z zakresu 1 do cnt – ilość rekordów, jakie chcemy pobrać, a pod :limit – wartość zmiennej limit.

Wadą tego rozwiązania jest jego „statyczność”, tzn dla każdego wylosowanego offsetu wynik zapytania będzie się powtarzał jeśli offest będzie się powtarzał. Jeśli jednak zawartość Twojej tabeli zmienia się dość często nie jest to problemem. Innym sposobem obejścia tego stanu rzeczy jest wykonywanie co jakiś czas „reindeksacji” kolumny random_num.

Tagi:

Komentarze (2)

NGINX z Symfony pod Windows

NGINX to szybki i lekki serwer HTTP, który mimo faktu, że zajmuje jedynie ok. 2 MB dysku, posiada sporo mozliwości konfiguracyjnych, takich jak virtual-hosts, czy url-rewriting. Dzięki FastCGI pozwala na obsługę skryptów PHP, ale i innych języków.
Pod windowsem, w domowej deweloperce króluje zestaw WAMP, w skład którego whodzi Windows, Apache, MySQL i PHP. Postanowiłem wykonać coś, co możnaby określić WNMP, czyli starego, ciężkiego Apacza zamienić na jego nowszy i lżejszy odpowiednik.
Całość jednak ma mi pozwalać kontynuwać prace nad kilkoma projektami, które tworzę z użyciem frameworka Symfony.

Jak uruchomić serwer NGINX ze wsparciem dla projektów Symfony pod Windows?

Ściągamy najnowszą wersję serwera NGINX – od niedawna istnieje wersja natywna, a nie cygwinowa, jakie były niedawno popularne. Natywna wersja powinnna być dużo szybsza od poprzedniczki. Serwer instalujemy w np. c:/dev/tools/nginx.

Zakładam, że system zarządzania bazą danych MySQL, framework Symfony, oraz sam PHP jest już zainstalowany na Twoim komputerze. Zakładam też, że zarówno MySQL, jak i PHP przygotowane są do współpracy z Symfony. Jeśli nie – w sieci jest mnóstwo opisów jak to zrobić.

Aby łatwo było uruchamiać i zatrzymywać serwer NGINX, musimy utworzyć następujące skrypty i zapisać je w katalogu NGINXa:

start-nginx.bat

@echo off
set NGINX_HOME=c:/dev/tools/nginx
set PHP_HOME=c:/dev/tools/php
set FASTCGI_ADDR=127.0.0.1
set FASTCGI_PORT=9000
 
echo "Startujemy nginx"
start %NGINX_HOME%/nginx.exe
echo "Startujemy php-cgi"
start /B %PHP_HOME%/php-cgi.exe -b %FASTCGI_ADDR%:%FASTCGI_PORT% -c %PHP_HOME%/php.ini
exit

stop-nginx.bat

@echo off
taskkill /f /IM nginx.exe
taskkill /f /IM php-cgi.exe
exit

Teraz wystarczy tylko skonfigurować NGINX tak, by

  • pozwalał na wykonywanie skryptów PHP za pomocą fastCGI
  • pozwalał na uruchamianie skryptów napisanych we frameworku Symfony

Edytujemy skrypt konfiguracyjny NGINXa (conf/nginx.conf):

worker_processes  1;
 
events {
    worker_connections  64;
}
 
http {
    include       mime.types;
    default_type  application/octet-stream;
 
    client_header_timeout   10m;
    client_body_timeout     10m;
    send_timeout            10m;
 
    connection_pool_size            256;
    client_header_buffer_size       1k;
    large_client_header_buffers     4 2k;
    request_pool_size               4k;
 
    gzip on;
    gzip_min_length 1100;
    gzip_buffers    4 8k;
    gzip_types      text/plain;
 
    output_buffers  1 32k;
    postpone_output 1460;
 
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
 
    keepalive_timeout       75 20;
    ignore_invalid_headers  on;
 
server {
    set  $docroot     c:/dev/php/symfony/myproject;
    root $docroot/web;
    index  index.php;
 
    listen       80;
    server_name  localhost;
 
    log_format main
                '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '"$gzip_ratio"'; 
    access_log  $docroot/log/myproject.access.log  main;
 
    charset utf-8;
 
    location / {
        if (-f $request_filename) {
            expires max;
            break;
        }
        rewrite ^(.*)/index.php last;
    }
 
    location /sf/ {
          root c:/dev/tools/php/data/symfony-1.2.7/data/web;
    }            
 
    location ~ \.php($|/) {
        set $the_uri $uri;
        if ($the_uri ~ "^(.+)/$") {
            set $the_uri    $1;
        }
 
        set  $script     $the_uri;
        set  $path_info  "";
 
        if ($uri ~ "^(.+\.php)(/.+)") {
            set  $script     $1;
            set  $path_info  $2;
        }
 
        fastcgi_index   index.php;
        fastcgi_pass   127.0.0.1:9000;
        include fastcgi_params;
 
        fastcgi_param  SCRIPT_FILENAME  $docroot/web$script;
        fastcgi_param  SCRIPT_NAME      $script;
        fastcgi_param  PATH_INFO        $path_info; 
        fastcgi_param  SERVER_NAME      $host;
    }
 
    location ~ /\.ht {
        deny  all;
    }
  }    
}

Pliczek konfiguracyjny NGINX’a jest dość czytelny i łatwo go zrozumieć (łatwo go też rozbudować i zoptymalizować jeśli np. używamy virtual-hostów). Na uwagę zasługuje jednak fakt, że przed przekazaniem argumentów do FastCGI musimy nieco „rozpracować” żądanie i wydobyć z niego zmienne $script i $path_info, ewentualnie usuwając zakańczający je znak slash (zmienna $the_uri). Warto też odnotować, że NGINX wspiera znane z Apacza aliasy – widoczna tu sekcja location /sf/.
Zauważ, że port 9000 wspomniany jest także w skrypcie startującym NGINX – możesz wybrać dowolny inny większy niż 1024 (niższe wymagają roli administratora i są zarezerwowane dla tzw. well-known-services).

Gotowe, teraz możemy uruchamiać NGINX poleceniem start-nginx.bat (z katalogu NGINXa) i zatrzymywać poprzez stop-nginx.bat.
Nasz projekt powinien być dostępny pod adresem http://localhost/ – czy to zakończonym slashem, czy tez nie.

Oczywiście nie jest to konfiguracja idealna – wspiera tylko jeden projekt (myproject), ale daje dobre podstawy do zbudowania konfiguracji dla wielu projektów i osobnych virtual-hostów dla każdego z nich. Warto wiedzieć, że konfiguracja NGINXa wspiera dyrektywę include, dzięki której możliwe jest importowanie konfiguracji z zewnętrznych plików konfiguracyjnych (np. po jednym dla każdego virtual-hosta) np. z sekcjami server.

Tagi: , , ,

Komentarze (2)

Uszczelnianie aplikacji z Grails i JSecurity – ciąg dalszy

Po ostatnim prostym zabezpieczeniu aplikacji Grailsowej z użyciem wtyczki JSecurity pozostał pewien niedosyt. Nasz przykład – i owszem – skutecznie zabezpieczył dostęp osób niezalogowanych, ale nie pokazał prawdziwej potęgi JSecurity – możliwości autoryzacji, czyli sprawdzania praw dostępu do określonych zasobów. Wyglądało to trochę tak, jakbyśmy wytaczali armatę po to by zabić muchę, dlatego teraz spróbujemy tą armatą zapolować na grubszego zwierza ;) wciąż jednak pozostając przy założeniu, że ma być prosto i estetycznie na ile to możliwe.
Przyjmijmy zatem, że nasza aplikacja ma:

  • dopuszczać tylko zalogowanych użytkowników (tak, jak poprzednio),
  • pozwalać na oglądanie zasobów wszystkim zalogowanym użytkownikom (posiadającym rolę „users”),
  • pozwalać na edycję/usuwanie/tworzenie nowych zasobów tylko użytkownikom posiadającym rolę „admin” (pozostali użytkownicy dostają informację o braku dostępu)

Jak widzisz, wykorzystamy szkielet poprzedniej aplikacji. Zauważ też, że wprowadzamy pojęcie roli, czym jest rola?
Zgodnie z infomacją na stronie wtyczki, to

a job or a set of responsibilities that a person can have

, czyli zbiór odpowiedzialności, które posiada dany użytkownik w systemie. System, oczywiście, musi takiemu użytkownikowi udostępnić możliwość wykonania czynności wchodzących w skład takich odpowiedzialności. Upraszczając – posiadanie danej roli pozwala na dostęp do zasobów systemu, do których dostęp bez niej jest zabroniony.
Założeniem naszej aplikacji jest, aby każdy zalogowany użytkownik mógł oglądać dane, jednak modyfikacja jakichkolwiek informacji wiąże się z obowiązkiem posiadania roli admina.

Zaczynajmy.

1. Dodajemy nowe klasy dziedzinowe

Musimy zdefiniować nowe klasy dziedzinowe, które pozwolą nam utrwalać informacje o rolach i powiązaniach ich z użytkownikami.

class Role {
    String name
}
class UserRoleRef {
    User user
    Role role
}

Dzięki takim klasom dziedzinowym (i klasie User z poprzedniego wpisu) możliwa będzie realizacja referencji wiele-do-wielu – jeden użytkownik może mieć wiele ról w systemie, a jedna rola może być w posiadaniu wielu użytkowników.

Możemy teraz zapisać wstępnie uprawnienia dostępu do naszego systemu, np. tak:

// użytkownicy
def jasiu = new User(username: "jasiu", password: new Sha1Hash("sekret").toHex()).save()
def admin = new User(username: "admin", password: new Sha1Hash("admin").toHex()).save()
 
// role
def usersRole = new Role("users").save()
def adminRole = new Role("admin").save()
 
// powiązania user-role
new UserRoleRef(user: jasiu, role: usersRole).save()
new UserRoleRef(user: admin, role: adminRole).save()

2. Uaktualniamy filtr

Role i powiązania z użytkownikami mamy już zdefiniowane i zapisane w bazie danych, jednak wciąż nie wiemy po co nam one. Samo posiadanie praw „admina” nic nie daje, jeśli nie wiemy, co one dają. Uaktualniamy nasz plik SecurityFilters.groovy tak:

class SecurityFilters {
    def filters = {
        auth(controller: "*", action: "*") {
            before = {
                accessControl { true }
            }
        }
 
        manageRecord(controller: "*", action: "(create|edit|save|update|delete)") {
            before = {
                accessControl {
                    role("admins")
                }
            }
        }
 
        showRecord(controller: "*", action: "show") {
            before = {
                accessControl {
                    role("admins") || role("users")
                }
            }
        }
    }
}

Jak widać, mamy zdefiniowane dwie reguły dostępu do dowolnego kontrolera (gwiazdka), ale z wyszczególnionymi akcjami. Akcja show wymaga posiadania roli „users”, albo „admins” (czyli dowolnej, na chwilę obecną), a akcje pozwalające na modyfikacje informacji – roli „admins”.

3. Rozszerzamy funkcjonalność MyRealm

Czas na fragment kodu, który będzie odpowiadał za sprawdzanie, czy dany użytkownik posiada daną rolę. Wzbogacamy naszą klasę MyRealm w dwie dodatkowe (poza authenticate()) metody:

    def hasRole(principal, roleName) {
        def criteria = UserRoleRef.createCriteria()
        def roles = criteria.list {
            role {
                eq('name', roleName)
            }
            user {
                eq('username', principal)
            }
        }
 
        return roles.size() > 0
    }
 
    def hasAllRoles(principal, roles) {
        def criteria = UserRoleRef.createCriteria()
        def r = criteria.list {
            role {
                'in'('name', roles)
            }
            user {
                eq('username', principal)
            }
        }
 
        return r.size() == roles.size()
    }

Obie są dość proste – pierwsza sprawdza istnienie referencji użytkownik-rola dla zadanych parametrów, druga metoda robi to samo, ale ze zbiorem ról zadanych jako parametr.

4. Dodajemy widok informujący o braku dostępu

Założenia mamy takie, żeby użytkownik bez dostępu do konkretnego zasobu został o tym poinformowany komunikatem. W tym celu wprowadzamy drobne usprawnienie do naszego kontrolera AuthController. Wprowadzamy domknięcie unauthorized, które będzie wykonane za każdym razem, gdy użytkownik spróbuje wykonać akcję, do której nie ma dostępu.

def unauthorized = {
}

… oraz plik widoku dla takiej akcji (views/auth/unauthorized.gsp):

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Brak dostępu</title>
  </head>
  <body>
    <h1>Dostęp zabroniony</h1>
  </body>
</html>

5. Uaktualnianie menu

Nasze zabezpieczanie właściwie się zakończyło, jednak warto jeszcze zadbać o dodatkową rzecz. Jeśli pracujemy na widokach wygenerowanych przez Grails (np. za pomocą komendy grails generate-all <klasa_dziedzinowa>), to każdy z widoków wyposażony jest w przyciski/odnośniki kierujące do stron pozwalających na modyfikację/dodawanie/usuwanie rekordów, np. takich:

<div class="nav">
    <span class="menuButton"><a class="home" href="${createLinkTo(dir:'')}">Home</a></span>
    <span class="menuButton">New Book</span>
</div>

Teoretycznie nic się nie stanie, jeśli tak to zostawimy, bo nawet kliknięcie na link New Book nie spowoduje wygenerowania formularza tworzenia nowej książki, tylko skieruje nas do strony z informacją o braku dostępu. Jednak… czego oczy nie widzą, tego sercu nie żal :) więc warto ukryć przed użytkownikiem te pozycje menu, które nie są mu potrzebne. Wykorzystamy więc znacznik jsec:hasRole:

<div class="nav">
    <span class="menuButton"><a class="home" href="${createLinkTo(dir:'')}">Home</a></span>
    <jsec:hasRole name="admins">
        <span class="menuButton"><g:link class="create" action="create">New Book</g:link></span>
    </jsec:hasRole>
</div>

Od tej pory odnośnik New Book pokaże się tylko użytkownikom posiadającym rolę „admins”. Takie modyfikacje możemy wprowadzić we wszystkich widokach naszej aplikacji.

I to właściwie wszystko jeśli chodzi o kontrolę ról za pomocą JSecurity w Grails.

Dość ciekawą rzeczą, która warta jest poznania, ale nie opisałem jej tutaj (miało być prosto, więc nie chciałem dodatkowo komplikować) jest kontrola uprawnień za pomocą JSecurity. Wtyczka dla Grails pozwala na kontrolę uprawnień nie tylko na poziomie kontroler/akcja, ale też np. dostępu do plików, albo innych zasobów systemowych.

Tagi: ,

Skomentuj

Proste uwierzytelnianie w Grails z JSecurity

Pisałem ostatnio aplikację webową, która wymagała bardzo prostego uwierzytelniania. Grails – jak się okazuje – ma wtyczkę JSecurity, dzięki której można realizować proste uwierzytelnianie, ale i bardziej skomplikowane operacje autoryzacji. Wtyczka JSecurity dla Grails jest wyposażona w fajny QuickStart, który to już po instalacji pozwala na całkiem kompleksowe zabezpieczenie aplikacji. Mi jednak potrzebny był ekstremalnie prosty mechanizm, na szczęście wtyczka zaoferowała coś w sam raz.

Poniższy wpis jest oparty właśnie na wspomnianym wyżej QuickStarcie, jednak został uproszczony i ograniczony jedynie do mechanizmu autentykacji uwierzytelnienia, nie zaś autoryzacji (kontroli dostępu do zasobów) użytkowników.

Dostęp do aplikacji z JSecurity jest realizowany za pomocą przestrzeni (ang. realms), można skonstruować mechanizm autentykacji uwierzytelnienia (i autoryzacji) na różne sposoby, np. za pomocą katalogów LDAP, loginów systemowych, usług sieciowych itd. My wykorzystamy najbardziej tradycyjny mechanizm: sprawdzania użytkowników zapisanych w bazie danych. JSecurity realizuje mechanizm weryfikacji dostępu do poszczególnych zasobów systemu za pomocą przynależności użytkowników do ról, jednak mój przykład będzie znacznie prostszy – wystarczy, że użytkownik będzie istniał w bazie danych, a już będzie miał dostęp do zasobów systemu.

Oto jak wzbogacić naszą aplikację o wymaganie autentykacji uwierzytelniania użytkowników, kompletnie nie zajmując się kwestiami autoryzacji dostępu do zasobów (wszyscy zalogowani użytkownicy mają dostęp):

1. Instalujemy wtyczkę

grails install-plugin jsecurity

2. Tworzymy klasę dziedzinową użytkownika:

class User {
    String username
    String password
 
    static mapping = {
        table "users"
    }
 
    public String toString() {
        username
    }
}

Używam tutaj mapowania na inną niż domyślna nazwę tabeli (domyślnie byłaby „user”, a ja dałem „users”) – taką kiedyś mi wpojono konwencję, by tabele miały nazwy w liczbie mnogiej. Z kolei metoda toString() przyda się nam nieco później.

Sama tabelka nie wystarczy, trzeba jeszcze użytkownika zapisać w bazie. Możemy to zrobić używając poniższego fragmentu kodu:

new User(username: "jasiu", password: new Sha1Hash("sekret").toHex()).save()

Jak widzisz, hasło jest kodowane za pomocą algorytmu Sha1 (kodowanie to wspierane jest przez JSecurity), nie obędzie się więc bez:

import org.jsecurity.crypto.hash.Sha1Hash

3.  Dodajemy do katalogu grails-app/conf/ pliczek SecurityFilters.groovy o zawartości:

class SecurityFilters {
    def filters = {
        auth(controller: "*", action: "*") {
            before = {
                accessControl { true }
            }
        }
    }
}

Ten filtr spowoduje, że wszystkie żądania (gwiazdki oznaczają dowolny kontroler, i dowolną akcję) zostaną poddane działaniu naszego filtra, a ten, przed wykonaniem żądania, wymusi na nim sprawdzenie praw dostępu.

4. W katalogu realms/ tworzymy klasę MyDbRealm, o postaci:

import org.jsecurity.authc.AccountException
import org.jsecurity.authc.IncorrectCredentialsException
import org.jsecurity.authc.UnknownAccountException
import org.jsecurity.authc.SimpleAccount
 
class MyDbRealm {
    static authTokenClass = org.jsecurity.authc.UsernamePasswordToken
 
    def authenticate(authToken) {
        def username = authToken.username;
        def user = User.findByUsername(username);
        if (!user) {
            throw new UnknownAccountException("Użytkownik ${username} nie istnieje w bazie danych")
        }
 
        def account = new SimpleAccount(username, user.password, "MyDbRealm")
        if (!credentialMatcher.doCredentialsMatch(authToken, account)) {
            log.info 'Nieprawidłowe hasło dla użytkownika ${user.username}'
            throw new IncorrectCredentialsException("Nieprawidłowe hasło dla użytkownika ${user.username}")
        }
        return user;
    }
}

5. Tworzymy kontroler naszego mechanizmu uwierzytelniania AuthController (zapisujemy go oczywiście w katalogu controllers/ naszej aplikacji):

import org.jsecurity.authc.AuthenticationException
import org.jsecurity.authc.UsernamePasswordToken
import org.jsecurity.SecurityUtils
 
class AuthController {
    def jsecSecurityManager
 
    def index = { redirect(action: 'login', params: params) }
 
    def login = {
        return [ username: params.username, targetUri: params.targetUri ]
    }
 
    def signIn = {
        def authToken = new UsernamePasswordToken(params.username, params.password)
 
        try{
            this.jsecSecurityManager.login(authToken)
            def targetUri = params.targetUri ?: "/"
            redirect(uri: targetUri)
        }
        catch (AuthenticationException ex){
            log.info "Błąd logowania użytkownika: '${params.username}'."
            flash.message = "Logowanie nieudane"
 
            def m = [ username: params.username ]
            if (params.targetUri) {
                m['targetUri'] = params.targetUri
            }
 
            redirect(action: 'login', params: m)
        }
    }
 
    def signOut = {
        SecurityUtils.subject?.logout()
        redirect(uri: '/')
    }
}

6. Tworzymy okienko logowania (views/auth/login.gsp):

 
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="layout" content="main" />
  <title>Login</title>
</head>
<body>
  <g:if test="${flash.message}">
    <div class="message">${flash.message}</div>
  </g:if>
  <g:form action="signIn">
    <input type="hidden" name="targetUri" value="${targetUri}" />
    <table>
      <tbody>
        <tr>
          <td>Username:</td>
          <td><input type="text" name="username" value="${username}" /></td>
        </tr>
        <tr>
          <td>Password:</td>
          <td><input type="password" name="password" value="" /></td>
        </tr>
        <tr>
          <td />
          <td><input type="submit" value="Zaloguj" /></td>
        </tr>
      </tbody>
    </table>
  </g:form>
</body>
</html>

7. Modyfikujemy nasz główny layout tak, by pokazał czy i jako kto jesteśmy zalogowani:

<jsec:isLoggedIn>
    <div>Witaj <jsec:principal/>! (<g:link controller="auth" action="signOut">Wyloguj</g:link>)</div>
</jsec:isLoggedIn>

I tu właśnie przydała nam się metoda toString() klasy User, albowiem znacznik jsec:principial zwróci nam właśnie wartość obiektu, który przedstawia zalogowanego aktualnie użytkownika (to, co zwróciła metoda authenticate() klasy MyDbRealm), a ten reprezentowany jest właśnie przez toString().
Tag jsec:isLoggedIn wraz z zawartością umieszczamy np. nad użyciem znacznika g:layoutBody. Dla chcących potestować inne tagi dostarczane przez wtyczkę polecam lekturę ich listy.

Gotowe, aplikacja zabezpieczona, a my możemy spać spokojnie :)

I na koniec, dwa linki, jako uzupełnienie tematu zabezpieczania aplikacji Grails z JSecurity:

Tagi: ,

Komentarze (4)

Upiększanie JTable

Przy okazji moich zabaw ze Swingiem – krótko o tym jak ulepszyć JTable.

Mamy sobie JTable taką:

przed

A będziemy chcieli taką:

po1

Wzbogacimy więc naszą tabelkę o bardzo ładne oznaczenie numerów wierszy, oraz kolorowanie co drugiego rekordu.

Zabieg pierwszy: kolorowanie co drugiego wiersza

Aby używać warunkowego kolorowania wierszy, musimy wskazać naszej tabelce, by wykorzystała obiekt klasy pochodnej od DefaultTableCellRenderer, np. takiej:

class ColorCellRenderer extends DefaultTableCellRenderer {
 
    @Override
    public Component getTableCellRendererComponent( JTable table, Object val, boolean selected, boolean focused, int row, int col ) {
        Component comp = super.getTableCellRendererComponent( table, val, selected, focused, row, col );
        if( selected == false ) {
            if( ( row % 2 ) == 1 ) {
                comp.setBackground( color );
            }
            else {
                comp.setBackground( null );
            }
        }
        return comp;
    }
}

Zabieg drugi: numeracja wierszy

Na forum Suna znalazłem b. stary, ale nadal ciekawy przykład rozwiązania tego problemu. Budujemy klasę LineNumberTable, która dziedziczy po JTable:

public class LineNumberTable extends JTable {
    private JTable mainTable;
 
    public LineNumberTable( JTable table ) {
        super();
        mainTable = table;
        setAutoCreateColumnsFromModel( false );
        setModel( mainTable.getModel() );
        setSelectionModel( mainTable.getSelectionModel() );
        setAutoscrolls( false );
 
        addColumn( new TableColumn() );
        getColumnModel().getColumn( 0 ).setCellRenderer( mainTable.getTableHeader().getDefaultRenderer() );
        getColumnModel().getColumn( 0 ).setPreferredWidth( 30 );
        setPreferredScrollableViewportSize( getPreferredSize() );
    }
 
    @Override
    public boolean isCellEditable( int row, int column ) {
        return false;
    }
 
    @Override
    public Object getValueAt( int row, int column ) {
        return new Integer( row + 1 );
    }
 
    @Override
    public int getRowHeight( int row ) {
        return mainTable.getRowHeight();
    }
}

OK, teraz wystarczy wykorzystać utworzone klasy w naszej JTable:

    public MyPanel() {
        initComponents();
        // wskazujemy nowy renderer, dla obiektów wszystkich typów naszej tabelki
        jTable1.setDefaultRenderer( Object.class, new ColorCellRenderer() );
        // scrollPane, którym mamy osadzoną tabelę dekorujemy oznaczeniem numerów wierszy
        jScrollPane1.setRowHeaderView( new LineNumberTable( jTable1 ) );
    }

Gotowe. Prawda, że ładniej?

Tagi: , ,

Komentarze (3)