2014-12-01 24 views
5

W aplikacji, w której aktualnie pracuję, konieczne jest wybranie jednej daty lub kropki z tego samego JavaFX 8 DatePicker.Wybieranie okresu lub daty przy użyciu JEDNEGO JavaFX 8 DatePicker

Preferowanym sposobem w ten sposób będzie w następujący sposób:

  1. Wybór jeden termin - tak samo jak domyślne zachowanie DatePicker.

  2. Wybór okresu - wybierz datę rozpoczęcia/zakończenia, przytrzymując przycisk myszy i przeciągnij do żądanej daty zakończenia/rozpoczęcia. Po zwolnieniu przycisku myszy zdefiniowano okres. Nie można zaakceptować dat innych niż wyświetlone.

  3. Montaż powinien działać zarówno dla pojedynczego daty (ex 24.12.2014) i okres (ex: 24.12.2014 - 27.12.2014)

Możliwym renderowania wybranego okresu (minus treści edytor tekstu) powyżej będzie wyglądać następująco:

Rendering of selected period

Gdzie pomarańczowy wskazuje aktualną datę, niebieski wskazuje wybrany okres. Zdjęcie pochodzi z wykonanego przeze mnie prototypu, ale w którym okres jest wybierany za pomocą dwóch formatów DatePickers zamiast jednego.

miałem przyjrzeć źródłowego

com.sun.javafx.scene.control.skin.DatePickerContent 

który ma

protected List<DateCell> dayCells = new ArrayList<DateCell>(); 

w celu znalezienia sposobu wykrywania gdy mysz wybrana data zakończenia, gdy mysz została wydana (lub może wykryć opór).

Jednak nie jestem do końca pewny, jak do tego dojść. Jakieś sugestie?

Dołączam prosty prototypowy kod, który do tej pory zrobiłem (który używa 2 zamiast pożądanego 1 datepika).

Prototype so far

import java.time.LocalDate; 

import javafx.beans.property.SimpleObjectProperty; 

public interface PeriodController { 

    /** 
    * @return Today. 
    */ 
    LocalDate currentDate(); 

    /** 
    * @return Selected from date. 
    */ 
    SimpleObjectProperty<LocalDate> fromDateProperty(); 

    /** 
    * @return Selected to date. 
    */ 
    SimpleObjectProperty<LocalDate> toDateProperty(); 
} 


import java.time.LocalDate; 
import java.time.format.DateTimeFormatter; 

import javafx.util.StringConverter; 

public class DateConverter extends StringConverter<LocalDate> { 

    private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); // TODO i18n 

    @Override 
    public String toString(LocalDate date) { 
     if (date != null) { 
      return dateFormatter.format(date); 
     } else { 
      return ""; 
     } 
    } 

    @Override 
    public LocalDate fromString(String string) { 
     if (string != null && !string.isEmpty()) { 
      return LocalDate.parse(string, dateFormatter); 
     } else { 
      return null; 
     } 
    } 


} 







import static java.lang.System.out; 

import java.time.LocalDate; 
import java.util.Locale; 

import javafx.application.Application; 
import javafx.beans.property.SimpleObjectProperty; 
import javafx.beans.value.ChangeListener; 
import javafx.geometry.HPos; 
import javafx.scene.Scene; 
import javafx.scene.control.Label; 
import javafx.scene.layout.GridPane; 
import javafx.scene.layout.VBox; 
import javafx.stage.Stage; 

public class PeriodMain extends Application { 

    private Stage stage; 

    public static void main(String[] args) { 
     Locale.setDefault(new Locale("no", "NO")); 
     launch(args); 
    } 

    @Override 
    public void start(Stage stage) { 
     this.stage = stage; 
     stage.setTitle("Period prototype "); 
     initUI(); 
     stage.getScene().getStylesheets().add(getClass().getResource("/period-picker.css").toExternalForm()); 
     stage.show(); 
    } 

    private void initUI() { 
     VBox vbox = new VBox(20); 
     vbox.setStyle("-fx-padding: 10;"); 
     Scene scene = new Scene(vbox, 400, 200); 


     stage.setScene(scene); 
     final PeriodPickerPrototype periodPickerPrototype = new PeriodPickerPrototype(new PeriodController() { 

      SimpleObjectProperty<LocalDate> fromDate = new SimpleObjectProperty<>(); 
      SimpleObjectProperty<LocalDate> toDate = new SimpleObjectProperty<>(); 

      { 
       final ChangeListener<LocalDate> dateListener = (observable, oldValue, newValue) -> { 
        if (fromDate.getValue() != null && toDate.getValue() != null) { 
         out.println("Selected period " + fromDate.getValue() + " - " + toDate.getValue()); 
        } 
       }; 
       fromDate.addListener(dateListener); 
       toDate.addListener(dateListener); 

      } 


      @Override public LocalDate currentDate() { 
       return LocalDate.now(); 
      } 

      @Override public SimpleObjectProperty<LocalDate> fromDateProperty() { 
       return fromDate; 
      } 

      @Override public SimpleObjectProperty<LocalDate> toDateProperty() { 
       return toDate; 
      } 


     }); 

     GridPane gridPane = new GridPane(); 
     gridPane.setHgap(10); 
     gridPane.setVgap(10); 
     Label checkInlabel = new Label("Check-In Date:"); 
     GridPane.setHalignment(checkInlabel, HPos.LEFT); 
     gridPane.add(periodPickerPrototype, 0, 1); 
     vbox.getChildren().add(gridPane); 
    } 
} 







import java.time.LocalDate; 

import javafx.beans.value.ChangeListener; 
import javafx.event.ActionEvent; 
import javafx.event.EventHandler; 
import javafx.scene.control.DateCell; 
import javafx.scene.control.DatePicker; 
import javafx.scene.control.Label; 
import javafx.scene.control.Tooltip; 
import javafx.scene.layout.GridPane; 
import javafx.util.Callback; 
import javafx.util.StringConverter; 


/** 
* Selecting a single date or a period - only a prototype. 
* As long as you have made an active choice on the {@code toDate}, the {@code fromDate} and {@code toDate} will have the same date. 
*/ 
public class PeriodPickerPrototype extends GridPane { 

    private static final String CSS_CALENDAR_BEFORE = "calendar-before"; 
    private static final String CSS_CALENDAR_BETWEEN = "calendar-between"; 
    private static final String CSS_CALENDAR_TODAY = "calendar-today"; 
    private static final boolean DISPLAY_WEEK_NUMBER = true; 

    private Label fromLabel; 
    private Label toLabel; 

    private DatePicker fromDate; 
    private DatePicker toDate; 
    private StringConverter<LocalDate> converter; 
    private PeriodController controller; 
    private ChangeListener<LocalDate> fromDateListener; 
    private ChangeListener<LocalDate> toDateListener; 
    private Callback<DatePicker, DateCell> toDateCellFactory; 
    private Callback<DatePicker, DateCell> fromDateCellFactory; 
    private Tooltip todayTooltip; 
    private boolean toDateIsActivlyChosenbyUser; 

    public PeriodPickerPrototype(final PeriodController periodController) 

    { 
     this.controller = periodController; 
     createComponents(); 
     makeLayout(); 
     createHandlers(); 
     bindAndRegisterHandlers(); 
     i18n(); 
     initComponent(); 
    } 

    public void createComponents() { 
     fromLabel = new Label(); 
     toLabel = new Label(); 
     fromDate = new DatePicker(); 
     toDate = new DatePicker(); 
     todayTooltip = new Tooltip(); 
    } 

    public void createHandlers() { 
     fromDate.setOnAction(event -> { 
      if ((!toDateIsActivlyChosenbyUser) || fromDate.getValue().isAfter(toDate.getValue())) { 
       setDateWithoutFiringEvent(fromDate.getValue(), toDate); 
       toDateIsActivlyChosenbyUser = false; 
      } 

     }); 

     toDate.setOnAction(event -> toDateIsActivlyChosenbyUser = true); 

     fromDateCellFactory = new Callback<DatePicker, DateCell>() { 
      @Override public DateCell call(final DatePicker datePicker) { 
       return new DateCell() { 
        @Override 
        public void updateItem(LocalDate item, boolean empty) { 
         super.updateItem(item, empty); 
         getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN); 

         if ((item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) && item.isAfter(fromDate.getValue())) { 
          getStyleClass().add(CSS_CALENDAR_BETWEEN); 
         } 

         if (item.isEqual(controller.currentDate())) { 
          getStyleClass().add(CSS_CALENDAR_TODAY); 
          setTooltip(todayTooltip); 
         } else { 
          setTooltip(null); 
         } 
        } 
       }; 
      } 
     }; 

     toDateCellFactory = 
       new Callback<DatePicker, DateCell>() { 
        @Override 
        public DateCell call(final DatePicker datePicker) { 
         return new DateCell() { 
          @Override 
          public void updateItem(LocalDate item, boolean empty) { 
           super.updateItem(item, empty); 
           setDisable(item.isBefore(fromDate.getValue())); 
           getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN); 


           if (item.isBefore(fromDate.getValue())) { 
            getStyleClass().add(CSS_CALENDAR_BEFORE); 
           } else if (item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) { 
            getStyleClass().add(CSS_CALENDAR_BETWEEN); 
           } 
           if (item.isEqual(controller.currentDate())) { 
            getStyleClass().add(CSS_CALENDAR_TODAY); 
            setTooltip(todayTooltip); 
           } else { 
            setTooltip(null); 
           } 
          } 
         }; 
        } 
       }; 
     converter = new DateConverter(); 
     fromDateListener = (observableValue, oldValue, newValue) -> { 
      if (newValue == null) { 
       // Restting old value and cancel.. 
       setDateWithoutFiringEvent(oldValue, fromDate); 
       return; 
      } 
      controller.fromDateProperty().set(newValue); 
     }; 
     toDateListener = (observableValue, oldValue, newValue) -> { 
      if (newValue == null) { 
       // Restting old value and cancel.. 
       setDateWithoutFiringEvent(oldValue, toDate); 
       return; 
      } 
      controller.toDateProperty().set(newValue); 
     }; 

    } 

    /** 
    * Changes the date on {@code datePicker} without fire {@code onAction} event. 
    */ 
    private void setDateWithoutFiringEvent(LocalDate newDate, DatePicker datePicker) { 
     final EventHandler<ActionEvent> onAction = datePicker.getOnAction(); 
     datePicker.setOnAction(null); 
     datePicker.setValue(newDate); 
     datePicker.setOnAction(onAction); 
    } 

    public void bindAndRegisterHandlers() { 
     toDate.setDayCellFactory(toDateCellFactory); 
     fromDate.setDayCellFactory(fromDateCellFactory); 
     fromDate.valueProperty().addListener(fromDateListener); 
     fromDate.setConverter(converter); 
     toDate.valueProperty().addListener(toDateListener); 
     toDate.setConverter(converter); 

    } 

    public void makeLayout() { 
     setHgap(6); 
     add(fromLabel, 0, 0); 
     add(fromDate, 1, 0); 
     add(toLabel, 2, 0); 
     add(toDate, 3, 0); 

     fromDate.setPrefWidth(120); 
     toDate.setPrefWidth(120); 
     fromLabel.setId("calendar-label"); 
     toLabel.setId("calendar-label"); 
    } 

    public void i18n() { 
     // i18n code replaced with 
     fromDate.setPromptText("dd.mm.yyyy"); 
     toDate.setPromptText("dd.mm.yyyy"); 
     fromLabel.setText("From"); 
     toLabel.setText("To"); 
     todayTooltip.setText("Today"); 
    } 

    public void initComponent() { 
     fromDate.setTooltip(null); // Ønsker ikke tooltip 
     setDateWithoutFiringEvent(controller.currentDate(), fromDate); 
     fromDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER); 

     toDate.setTooltip(null); // Ønsker ikke tooltip 
     setDateWithoutFiringEvent(controller.currentDate(), toDate); 
     toDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER); 
    } 


} 

/** period-picker.css goes udner resources (using maven) **/ 

.date-picker { 
    /* -fx-font-size: 11pt;*/ 
} 

.calendar-before { 
} 

.calendar-between { 
    -fx-background-color: #bce9ff; 
} 

.calendar-between:hover { 
    -fx-background-color: rgb(0, 150, 201); 
} 

.calendar-between:focused { 
    -fx-background-color: rgb(0, 150, 201); 
} 

.calendar-today { 
    -fx-background-color: rgb(255, 218, 111); 
} 

.calendar-today:hover { 
    -fx-background-color: rgb(0, 150, 201); 
} 

.calendar-today:focused { 
    -fx-background-color: rgb(0, 150, 201); 
} 

#calendar-label { 
    -fx-font-style: italic; 
    -fx-fill: rgb(75, 75, 75); 
    -fx-font-size: 11; 
} 

Odpowiedz

5

myślę, że jesteś już na dobrej drodze ... DateCell i przeciągnij może pracować, ponieważ okienko nie jest zamknięta, jeśli zostanie wykryte zdarzenie lub przeciągając kiedy kończy. Daje to możliwość śledzenia komórek wybranych przez użytkownika.

To szybki hack, ale może pomóc w wyborze zakresu.

Najpierw otrzyma zawartość i listę wszystkich komórek w wyświetlanym miesiącu, dodając słuchacza do zdarzeń przeciągania, oznaczając jako pierwszą komórkę, w której rozpoczyna się przeciąganie, i wybierając wszystkie komórki w obrębie tej pierwszej komórki i komórka pod rzeczywistą pozycją myszy, odznaczając resztę.

Po zakończeniu zdarzenia przeciągania wybrany zakres jest wyświetlany na konsoli. I możesz zacząć wszystko od nowa, aż popup zostanie zamknięty.

private DateCell iniCell=null; 
private DateCell endCell=null; 

@Override 
public void start(Stage primaryStage) { 
    DatePicker datePicker=new DatePicker(); 
    datePicker.setValue(LocalDate.now()); 

    Scene scene = new Scene(new AnchorPane(datePicker), 300, 250); 

    primaryStage.setScene(scene); 
    primaryStage.show(); 

    datePicker.showingProperty().addListener((obs,b,b1)->{ 
     if(b1){ 
      DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent(); 

      List<DateCell> cells = content.lookupAll(".day-cell").stream() 
        .filter(ce->!ce.getStyleClass().contains("next-month")) 
        .map(n->(DateCell)n) 
        .collect(Collectors.toList()); 

      content.setOnMouseDragged(e->{ 
       Node n=e.getPickResult().getIntersectedNode(); 
       DateCell c=null; 
       if(n instanceof DateCell){ 
        c=(DateCell)n; 
       } else if(n instanceof Text){ 
        c=(DateCell)(n.getParent()); 
       } 
       if(c!=null && c.getStyleClass().contains("day-cell") && 
         !c.getStyleClass().contains("next-month")){ 
        if(iniCell==null){ 
         iniCell=c; 
        } 
        endCell=c; 
       } 
       if(iniCell!=null && endCell!=null){ 
        int ini=(int)Math.min(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        int end=(int)Math.max(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        cells.stream() 
         .forEach(ce->ce.getStyleClass().remove("selected")); 
        cells.stream() 
         .filter(ce->Integer.parseInt(ce.getText())>=ini) 
         .filter(ce->Integer.parseInt(ce.getText())<=end) 
         .forEach(ce->ce.getStyleClass().add("selected")); 
       } 
      }); 
      content.setOnMouseReleased(e->{ 
       if(iniCell!=null && endCell!=null){ 
        System.out.println("Selection from "+iniCell.getText()+" to "+endCell.getText()); 
       } 
       endCell=null; 
       iniCell=null;      
      }); 
     } 
    }); 
} 

I tak to wygląda:

Range selection on DatePicker

Na razie to nie aktualizuje pole tekstowe, gdyż wiąże się to z użyciem niestandardowego formatowania.

EDIT

Dodałem konwerter ciąg niestandardowy, aby pokazać zakres w polu tekstowym, po wybór jest zrobione, a także wybrać zakres jeśli jeden ważny jest wpisany.

To nie jest punktor kuloodporny, ale działa jako dowód koncepcji.

private DateCell iniCell=null; 
private DateCell endCell=null; 

private LocalDate iniDate; 
private LocalDate endDate; 
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.MM.uuuu", Locale.ENGLISH);  

@Override 
public void start(Stage primaryStage) { 
    DatePicker datePicker=new DatePicker(); 
    datePicker.setValue(LocalDate.now()); 
    datePicker.setConverter(new StringConverter<LocalDate>() { 

     @Override 
     public String toString(LocalDate object) { 
      if(iniDate!=null && endDate!=null){ 
       return iniDate.format(formatter)+" - "+endDate.format(formatter); 
      } 
      return object.format(formatter); 
     } 

     @Override 
     public LocalDate fromString(String string) { 
      if(string.contains("-")){ 
       try{ 
        iniDate=LocalDate.parse(string.split("-")[0].trim(), formatter); 
        endDate=LocalDate.parse(string.split("-")[1].trim(), formatter); 
       } catch(DateTimeParseException dte){ 
        return LocalDate.parse(string, formatter); 
       } 
       return iniDate; 
      } 
      return LocalDate.parse(string, formatter); 
     } 
    }); 
    Scene scene = new Scene(new AnchorPane(datePicker), 300, 250); 

    primaryStage.setScene(scene); 
    primaryStage.show(); 

    datePicker.showingProperty().addListener((obs,b,b1)->{ 
     if(b1){ 
      DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent(); 

      List<DateCell> cells = content.lookupAll(".day-cell").stream() 
        .filter(ce->!ce.getStyleClass().contains("next-month")) 
        .map(n->(DateCell)n) 
        .collect(Collectors.toList()); 

      // select initial range 
      if(iniDate!=null && endDate!=null){ 
       int ini=iniDate.getDayOfMonth(); 
       int end=endDate.getDayOfMonth(); 
       cells.stream() 
        .forEach(ce->ce.getStyleClass().remove("selected")); 
       cells.stream() 
        .filter(ce->Integer.parseInt(ce.getText())>=ini) 
        .filter(ce->Integer.parseInt(ce.getText())<=end) 
        .forEach(ce->ce.getStyleClass().add("selected")); 
      } 
      iniCell=null; 
      endCell=null; 
      content.setOnMouseDragged(e->{ 
       Node n=e.getPickResult().getIntersectedNode(); 
       DateCell c=null; 
       if(n instanceof DateCell){ 
        c=(DateCell)n; 
       } else if(n instanceof Text){ 
        c=(DateCell)(n.getParent()); 
       } 
       if(c!=null && c.getStyleClass().contains("day-cell") && 
         !c.getStyleClass().contains("next-month")){ 
        if(iniCell==null){ 
         iniCell=c; 
        } 
        endCell=c; 
       } 
       if(iniCell!=null && endCell!=null){ 
        int ini=(int)Math.min(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        int end=(int)Math.max(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        cells.stream() 
         .forEach(ce->ce.getStyleClass().remove("selected")); 
        cells.stream() 
         .filter(ce->Integer.parseInt(ce.getText())>=ini) 
         .filter(ce->Integer.parseInt(ce.getText())<=end) 
         .forEach(ce->ce.getStyleClass().add("selected")); 
       } 
      }); 
      content.setOnMouseReleased(e->{ 
       if(iniCell!=null && endCell!=null){ 
        iniDate=LocalDate.of(datePicker.getValue().getYear(), 
             datePicker.getValue().getMonth(), 
             Integer.parseInt(iniCell.getText())); 
        endDate=LocalDate.of(datePicker.getValue().getYear(), 
             datePicker.getValue().getMonth(), 
             Integer.parseInt(endCell.getText())); 
        System.out.println("Selection from "+iniDate+" to "+endDate); 

        datePicker.setValue(iniDate); 
        int ini=iniDate.getDayOfMonth(); 
        int end=endDate.getDayOfMonth(); 
        cells.stream() 
         .forEach(ce->ce.getStyleClass().remove("selected")); 
        cells.stream() 
         .filter(ce->Integer.parseInt(ce.getText())>=ini) 
         .filter(ce->Integer.parseInt(ce.getText())<=end) 
         .forEach(ce->ce.getStyleClass().add("selected")); 
       } 
       endCell=null; 
       iniCell=null;     
      }); 
     } 
    }); 
} 

Range selection and edition

+0

Było to zgodne z tym, co szukałem. W dalszej części opublikuję bardziej niezawodne rozwiązanie. Muszę być w stanie wybrać dni do następnego miesiąca, więc zamiast używać "getText" do wyprowadzenia daty, użyję refleksji, aby uzyskać dostęp do "dayCellDate" w "DatePickerContent", aby poprawnie zidentyfikować "LocalDate". Ponadto, kiedy wybór zostanie dokonany, powinien on zostać zamknięty, więc dodam 'datePicker.hide();'. Powinno działać zarówno wstecz, jak i do przodu, więc dodanie pierwszej/ostatniej metody pozwoli ci wybrać 25 pierwszych i 11 ostatnich (w przykładzie). Doceniam twój dowód koncepcji. – Skjalg

+0

Dzięki. Do 'następnego miesiąca' masz na myśli wybór dni już pokazanych w tej samej sieci, prawda? Właśnie usunąłem je z selekcji komórek dla uproszczenia. Mają styl 'następnego miesiąca', więc nie będziesz potrzebował refleksji, tylko ustaw dla nich' datePicker.getValue(). GetMonth() + 1'' –

+0

Tak, mam na myśli wybór dni pokazanych allready. Na powyższym zdjęciu możesz wybrać na przykład 31. grudnia do 3. stycznia. Zdałem sobie sprawę, że "następny miesiąc" był dla uproszczenia. Zdaję sobie sprawę, że wielu ludzi sprzeciwia się użyciu refleksji w OO, ale w moim przekonaniu bardziej sensowne jest tu użycie istniejącej metody, niż jej obliczenie, jakkolwiek by nie było. – Skjalg

Powiązane problemy