Проблемы точности Java JSlider - PullRequest
4 голосов
/ 01 октября 2011

У меня есть список из N JSliders (N не изменяется процедурно, только когда я добавляю больше возможностей. В настоящее время N равно 4). Сумма всех значений ползунков должна равняться 100. При перемещении одного ползунка остальные ползунки должны корректироваться. Каждый ползунок имеет значения в диапазоне от 0 до 100.

В настоящее время я использую эту логику при смене слайдера (псевдокод):

newValue = currentSlider.getValue

otherSliders = allSliders sans currentSlider
othersValue = summation of otherSliders values
properOthersValue = 100 - newValue

ratio = properOthersValue / othersValue

for slider in  otherSlider 
    slider.value = slider.getValue * ratio

Проблема с этой настройкой заключается в том, что значения ползунка хранятся в виде целых чисел. Поэтому, когда я настраиваю ползунки, у меня возникают проблемы с точностью: ползунки будут дергаться или вообще не двигаться в зависимости от значения отношения. Кроме того, общая стоимость не всегда составляет до 100.

У кого-нибудь есть решение этой проблемы без создания совершенно нового класса JSlider, который поддерживает числа с плавающей запятой или двойные числа?

Если вам нужен пример поведения, которое я хочу, посетите: Humble Indie Bundle и прокрутите страницу до конца.

спасибо

p.s. Умножение значений на коэффициент позволяет пользователю «заблокировать» значения на 0. Однако я не уверен, что делать, когда 3 из 4 ползунков находятся на 0, а 4-й ползунок на 100, и я перемещаю 4-й ползунок вниз , Используя приведенную выше логику, 3 ползунка с 0 в качестве значения остаются на месте, а 4-й ползунок перемещается туда, куда его помещает пользователь, что в итоге составляет менее 100, что является неправильным поведением.

EDIT

Вот SSCCE:

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.util.LinkedList;

public class SliderDemo
{
    static LinkedList<JSlider> sliders = new LinkedList<JSlider>();

    static class SliderListener implements ChangeListener
    {
        boolean updating = false;

        public void stateChanged(ChangeEvent e)
        {
            if (updating) return;
            updating = true;

            JSlider source = (JSlider)e.getSource();

            int newValue = source.getValue();
            LinkedList<JSlider> otherSliders = new LinkedList<JSlider>(sliders);
            otherSliders.remove(source);

            int otherValue = 0;
            for (JSlider slider : otherSliders)
            {
                otherValue += slider.getValue();
            }

            int properValue = 100 - newValue;
            double ratio = properValue / (double)otherValue;

            for (JSlider slider : otherSliders)
            {
                int currentValue = slider.getValue();
                int updatedValue = (int) (currentValue * ratio);
                slider.setValue(updatedValue);
            }

            int total = 0;
            for (JSlider slider : sliders)
            {
                total += slider.getValue();
            }
            System.out.println("Total = " + total);

            updating = false;
        }
    }

    public static void main(String[] args)
    {
        JFrame frame = new JFrame("SliderDemo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Container container = frame.getContentPane();
        JPanel sliderPanel = new JPanel(new GridBagLayout());
        container.add(sliderPanel);

        SliderListener listener = new SliderListener();

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        int sliderCount = 4;
        int initial = 100 / sliderCount;
        for (int i = 0; i < sliderCount; i++)
        {
            gbc.gridy = i;
            JSlider slider = new JSlider(0, 100, initial);
            slider.addChangeListener(listener);
            slider.setMajorTickSpacing(50);
            slider.setPaintTicks(true);
            sliders.add(slider);
            sliderPanel.add(slider, gbc);
        }

        frame.pack();
        frame.setVisible(true);
    }
}

Ответы [ 3 ]

6 голосов
/ 01 октября 2011

Почему бы не сделать гранулярность моделей JSlider более точной, скажем, чтобы они были от 0 до 1000000, а сумма - 1000000?При правильном Dictionary для LabelTable пользователь, вероятно, не будет знать, что он не изменяется от 0 до 100.

Например:

import java.awt.Dimension;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

@SuppressWarnings("serial")
public class LinkedSliders2 extends JPanel {
   private static final int SLIDER_COUNT = 5;
   public static final int SLIDER_MAX_VALUE = 1000;
   private static final int MAJOR_TICK_DIVISIONS = 5;
   private static final int MINOR_TICK_DIVISIONS = 20;
   private static final int LS_WIDTH = 700;
   private static final int LS_HEIGHT = 500;
   private JSlider[] sliders = new JSlider[SLIDER_COUNT];
   private SliderGroup2 sliderGroup = new SliderGroup2(SLIDER_MAX_VALUE);

   public LinkedSliders2() {
      Dictionary<Integer, JComponent> myDictionary = new Hashtable<Integer, JComponent>();
      for (int i = 0; i <= MAJOR_TICK_DIVISIONS; i++) {
         Integer key = i * SLIDER_MAX_VALUE / MAJOR_TICK_DIVISIONS;
         JLabel value = new JLabel(String.valueOf(i * 100 / MAJOR_TICK_DIVISIONS));
         myDictionary.put(key, value);
      }
      setLayout(new GridLayout(0, 1));
      for (int i = 0; i < sliders.length; i++) {
         sliders[i] = new JSlider(0, SLIDER_MAX_VALUE, SLIDER_MAX_VALUE
               / SLIDER_COUNT);
         sliders[i].setLabelTable(myDictionary );
         sliders[i].setMajorTickSpacing(SLIDER_MAX_VALUE / MAJOR_TICK_DIVISIONS);
         sliders[i].setMinorTickSpacing(SLIDER_MAX_VALUE / MINOR_TICK_DIVISIONS);
         sliders[i].setPaintLabels(true);
         sliders[i].setPaintTicks(true);
         sliders[i].setPaintTrack(true);
         sliderGroup.addSlider(sliders[i]);
         add(sliders[i]);
      }
   }

   @Override
   public Dimension getPreferredSize() {
      return new Dimension(LS_WIDTH, LS_HEIGHT);
   }

   private static void createAndShowGui() {
      LinkedSliders2 mainPanel = new LinkedSliders2();

      JFrame frame = new JFrame("LinkedSliders");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

class SliderGroup2 {
   private List<BoundedRangeModel> sliderModelList = new ArrayList<BoundedRangeModel>();
   private ChangeListener changeListener = new SliderModelListener();
   private int maxValueSum;

   public SliderGroup2(int maxValueSum) {
      this.maxValueSum = maxValueSum;
   }

   public void addSlider(JSlider slider) {
      BoundedRangeModel model = slider.getModel();
      sliderModelList.add(model);
      model.addChangeListener(changeListener);
   }

   private class SliderModelListener implements ChangeListener {
      private boolean internalChange = false;

      @Override
      public void stateChanged(ChangeEvent cEvt) {
         if (!internalChange) {
            internalChange = true;
            BoundedRangeModel sourceModel = (BoundedRangeModel) cEvt.getSource();
            int sourceValue = sourceModel.getValue();

            int oldSumOfOtherSliders = 0;
            for (BoundedRangeModel model : sliderModelList) {
               if (model != sourceModel) {
                  oldSumOfOtherSliders += model.getValue();
               }
            }
            if (oldSumOfOtherSliders == 0) {
               for (BoundedRangeModel model : sliderModelList) {
                  if (model != sourceModel) {
                     model.setValue(1);
                  }
               }
               internalChange = false;
               return;
            }

            int newSumOfOtherSliders = maxValueSum - sourceValue;

            for (BoundedRangeModel model : sliderModelList) {
               if (model != sourceModel) {
                  long newValue = ((long) newSumOfOtherSliders * model
                        .getValue()) / oldSumOfOtherSliders;
                  model.setValue((int) newValue);
               }
            }

            int total = 0;
            for (BoundedRangeModel model : sliderModelList) {
               total += model.getValue();
            }
            //!! System.out.printf("Total = %.0f%n", (double)total * 100 / LinkedSliders2.SLIDER_MAX_VALUE);

            internalChange = false;
         }
      }

   }

}

Отредактировано для использования SliderGroup2список BoundedRangeModels, а не JSliders.

2 голосов
/ 02 октября 2011

Заимствуя некоторые решения на воздушной подушке, я предложил другой подход. Основой этого подхода является то, что значения «других ползунков» отслеживаются во время перемещения ползунка. Пока вы продолжаете скользить по тому же ползунку, замороженные значения используются для расчета новых значений. Любые различия округления затем применяются последовательно к каждому ползунку, пока разница не будет израсходована. Используя этот подход, вы можете иметь постепенные изменения в ползунке, примененные равномерно ко всем другим ползункам.

Значения в модели являются фактическими значениями ползунка, и вы также можете использовать клавиатуру для регулировки ползунков:

import java.awt.*;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.List;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;


public class SliderGroup implements ChangeListener
{
    private List<JSlider> sliders = new ArrayList<JSlider>();
    private int groupSum;

    private boolean internalChange = false;
    private JSlider previousSlider;
    private List<SliderInfo> otherSliders = new ArrayList<SliderInfo>();

    public SliderGroup(int groupSum)
    {
        this.groupSum = groupSum;
    }

    public void addSlider(JSlider slider)
    {
        sliders.add(slider);
        slider.addChangeListener(this);
    }

    @Override
    public void stateChanged(ChangeEvent e)
    {
        if (internalChange) return;

        internalChange = true;
        JSlider sourceSlider = (JSlider)e.getSource();

        if (previousSlider != sourceSlider)
        {
            setupForSliding(sourceSlider);
            previousSlider = sourceSlider;
        }

        int newSumOfOtherSliders = groupSum - sourceSlider.getValue();
        int oldSumOfOtherSliders = 0;

        for (SliderInfo info : otherSliders)
        {
            JSlider slider = info.getSlider();

            if (slider != sourceSlider)
            {
                oldSumOfOtherSliders += info.getValue();
            }
        }

        int difference = newSumOfOtherSliders - oldSumOfOtherSliders;

        if (oldSumOfOtherSliders == 0)
        {
            resetOtherSliders( difference / otherSliders.size() );
            allocateDifference(difference % otherSliders.size(), true);
            internalChange = false;
            return;
        }

        double ratio = (double)newSumOfOtherSliders / oldSumOfOtherSliders;

        for (SliderInfo info : otherSliders)
        {
                JSlider slider = info.getSlider();
                int oldValue = info.getValue();
                int newValue = (int)Math.round(oldValue * ratio);
                difference += oldValue - newValue;
                slider.getModel().setValue( newValue );
        }

        if (difference != 0)
        {
            allocateDifference(difference, false);
        }

        internalChange = false;
    }

    private void allocateDifference(int difference, boolean adjustZeroValue)
    {
        while (difference != 0)
        {
            for (SliderInfo info : otherSliders)
            {
                if (info.getValue() != 0 || adjustZeroValue)
                {
                    JSlider slider = info.getSlider();

                    if (difference > 0)
                    {
                        slider.getModel().setValue( slider.getValue() + 1 );
                        difference--;
                    }

                    if (difference < 0)
                    {
                        slider.getModel().setValue( slider.getValue() - 1 );
                        difference++;
                    }
                }
            }
        }
    }

    private void resetOtherSliders(int resetValue)
    {
        for (SliderInfo info : otherSliders)
        {
            JSlider slider = info.getSlider();
            slider.getModel().setValue( resetValue );
        }
    }

    private void setupForSliding(JSlider sourceSlider)
    {
        otherSliders.clear();

        for (JSlider slider: sliders)
        {
            if (slider != sourceSlider)
            {
                otherSliders.add( new SliderInfo(slider, slider.getValue() ) );
            }
        }
    }

    class SliderInfo
    {
        private JSlider slider;
        private int value;

        public SliderInfo(JSlider slider, int value)
        {
            this.slider = slider;
            this.value = value;
        }

        public JSlider getSlider()
        {
            return slider;
        }

        public int getValue()
        {
            return value;
        }
    }


    private static JPanel createSliderPanel(int groupSum, int sliderCount)
    {
        int sliderValue = groupSum / sliderCount;

        SliderGroup sg = new SliderGroup(groupSum);

        JPanel panel = new JPanel( new BorderLayout() );

        JPanel sliderPanel = new JPanel( new GridLayout(0, 1) );
        panel.add(sliderPanel, BorderLayout.CENTER);

        JPanel labelPanel = new JPanel( new GridLayout(0, 1) );
        panel.add(labelPanel, BorderLayout.EAST);

        for (int i = 0; i < sliderCount; i++)
        {
            JLabel label = new JLabel();
            label.setText( Integer.toString(sliderValue) );
            labelPanel.add( label );

            JSlider slider = new JSlider(0, groupSum, sliderValue);
            slider.setMajorTickSpacing(25);
            slider.setMinorTickSpacing(5);
            slider.setPaintTicks(true);
            slider.setPaintLabels(true);
            slider.setPaintTrack(true);
            slider.addChangeListener( new LabelChangeListener(label) );
            sliderPanel.add( slider );

            sg.addSlider( slider );
        }

        return panel;
    }

    static class LabelChangeListener implements ChangeListener
    {
        private JLabel label;

        public LabelChangeListener(JLabel label)
        {
            this.label = label;
        }

        @Override
        public void stateChanged(ChangeEvent e)
        {
            JSlider slider = (JSlider)e.getSource();
            label.setText( Integer.toString(slider.getValue()) );
        }
    }

    private static void createAndShowGui()
    {
        JPanel panel = createSliderPanel(100, 5);

        JFrame frame = new JFrame("SliderGroup");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(panel);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGui();
            }
        });
    }
}
2 голосов
/ 01 октября 2011

ползунки будут дергаться или не двигаться вообще в зависимости от значения отношения.

У HumbleBundle та же проблема. Если вы переместите ползунок за клавиатуру, тогда изменение будет равно только 1, что означает, что оно будет переходить только к первому ползунку. Таким образом, ваши коэффициенты будут в конечном итоге не синхронизированы.

Также общая стоимость не всегда составляет 100.

Так что вам нужно сделать проверку округления. Если это не добавляет к 100, то вам нужно решить, куда идет ошибка. Может быть, последний слайдер с учетом вышеуказанной проблемы?

Я не уверен, что делать, когда 3 из 4 ползунков установлены на 0, а 4-й ползунок на 100, и я перемещаю 4-й ползунок вниз.

Способ, которым HumbleBundle обращается с ним, заключается в перемещении всех слайсеров. Однако он позволяет только перемещать ползунок вниз на 3, чтобы вы могли увеличить каждый из 3 ползунков на 1.

Даже реализация в HumbleBundle не идеальна.

...